initial framework

This commit is contained in:
2026-02-11 21:05:26 +00:00
parent 1df6c91109
commit f318513b2f
17 changed files with 1754 additions and 63 deletions

View File

@ -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
npm run dev
@ -14,21 +14,4 @@ pnpm dev
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
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.
Development server opens on [localhost:5000](http://localhost:5000)

31
actions.ts Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,9 +1,25 @@
import type { Metadata } from "next";
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 = {
title: "Create Next App",
description: "Generated by create next app",
title: "LMGCITFY",
description: "Let me GCI that for you.",
};
export default function RootLayout({
@ -12,8 +28,15 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
<html lang="en" suppressHydrationWarning={true}>
<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>
);
}

View File

@ -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() {
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 (
<main>
<div>Hello world!</div>
</main>
<ChakraProvider value={theme}>
<Container backgroundColor={'#2a2a2a'} borderRadius={'lg'} boxShadow={'0 0 12px #4a4a4a'} py={4} width={{ base: '90%', md: '50%' }} overflow={'hidden'}>
<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&apos;m feeling free</Button>
</Flex>
</Container>
</ChakraProvider>
);
}

View 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}
/>
)
},
)

View 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
View 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
View 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>
)
},
)

View File

@ -2,6 +2,9 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
experimental: {
optimizePackageImports: ["@chakra-ui/react"],
},
};
export default nextConfig;

1399
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,22 @@
{
"name": "lmgcitfy",
"version": "0.1.0",
"description": "Helping people use the search feature on GunCAD Index",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 5000",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@chakra-ui/react": "^3.32.0",
"@emotion/react": "^11.14.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"react-icons": "^5.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

BIN
public/background.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
public/gci_logo_large.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

17
theme.ts Normal file
View 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);

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

20
types.d.ts vendored Normal file
View 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 };