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
|
||||
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
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 "./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>
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
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'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 = {
|
||||
/* config options here */
|
||||
experimental: {
|
||||
optimizePackageImports: ["@chakra-ui/react"],
|
||||
},
|
||||
};
|
||||
|
||||
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",
|
||||
"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
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": {
|
||||
"target": "ES2017",
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": 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