├── utils ├── constants.ts ├── supabase_admin.ts ├── prompts.ts ├── hacker_news_api.ts ├── ai_wrapper.ts ├── api.ts └── analytics.ts ├── README.md ├── components ├── ui │ ├── Input │ │ ├── index.ts │ │ ├── Input.module.css │ │ └── Input.tsx │ ├── Button │ │ ├── index.ts │ │ ├── Button.module.css │ │ └── Button.tsx │ ├── Footer │ │ ├── index.ts │ │ └── Footer.tsx │ ├── LoadingDots │ │ ├── index.ts │ │ ├── LoadingDots.tsx │ │ └── LoadingDots.module.css │ ├── Card.tsx │ ├── CodeWrapper.tsx │ ├── RecentSummaries.tsx │ ├── HowItWorks │ │ └── Faq.tsx │ └── Hero │ │ └── Hero.tsx ├── icons │ ├── Logo.tsx │ └── GitHub.tsx └── Layout.tsx ├── .gitattributes ├── postcss.config.js ├── public └── fonts │ └── Manrope │ ├── Manrope-Bold.ttf │ ├── Manrope-Light.ttf │ ├── Manrope-Medium.ttf │ ├── Manrope-Regular.ttf │ ├── Manrope-ExtraBold.ttf │ ├── Manrope-ExtraLight.ttf │ └── Manrope-SemiBold.ttf ├── test.md ├── next-env.d.ts ├── styles ├── chrome-bug.css └── main.css ├── pages ├── _document.tsx ├── index.tsx ├── _app.tsx ├── [id].tsx └── api │ └── ai │ └── gpt3.ts ├── next.config.js ├── types.ts ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── tailwind.config.js ├── schema.sql └── yarn.lock /utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const getStartedLink = "/try-it-out"; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hacker-news-tldr 2 | 3 | It is all in Hero.tsx. 4 | -------------------------------------------------------------------------------- /components/ui/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Input'; 2 | -------------------------------------------------------------------------------- /components/ui/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Button'; 2 | -------------------------------------------------------------------------------- /components/ui/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Footer'; 2 | -------------------------------------------------------------------------------- /components/ui/LoadingDots/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LoadingDots'; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {} 6 | } 7 | }; -------------------------------------------------------------------------------- /public/fonts/Manrope/Manrope-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SideGuide/hacker-news-tldr/HEAD/public/fonts/Manrope/Manrope-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Manrope/Manrope-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SideGuide/hacker-news-tldr/HEAD/public/fonts/Manrope/Manrope-Light.ttf -------------------------------------------------------------------------------- /public/fonts/Manrope/Manrope-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SideGuide/hacker-news-tldr/HEAD/public/fonts/Manrope/Manrope-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/Manrope/Manrope-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SideGuide/hacker-news-tldr/HEAD/public/fonts/Manrope/Manrope-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/Manrope/Manrope-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SideGuide/hacker-news-tldr/HEAD/public/fonts/Manrope/Manrope-ExtraBold.ttf -------------------------------------------------------------------------------- /public/fonts/Manrope/Manrope-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SideGuide/hacker-news-tldr/HEAD/public/fonts/Manrope/Manrope-ExtraLight.ttf -------------------------------------------------------------------------------- /public/fonts/Manrope/Manrope-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SideGuide/hacker-news-tldr/HEAD/public/fonts/Manrope/Manrope-SemiBold.ttf -------------------------------------------------------------------------------- /test.md: -------------------------------------------------------------------------------- 1 | hi theredaw [here](https://www.google.com) is a link 2 | 3 | but there is also this broken one [here](https://sideguide.dezxcadwo) is a link 4 | -------------------------------------------------------------------------------- /components/icons/Logo.tsx: -------------------------------------------------------------------------------- 1 | const Logo = ({ className = '', ...props }) => ( 2 | 3 | ); 4 | 5 | export default Logo; 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/Input/Input.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply bg-black py-2 px-3 w-full appearance-none transition duration-150 ease-in-out border border-zinc-500 text-zinc-200; 3 | } 4 | 5 | .root:focus { 6 | @apply outline-none; 7 | } 8 | -------------------------------------------------------------------------------- /utils/supabase_admin.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | 3 | const supabaseAdmin = createClient( 4 | process.env.NEXT_PUBLIC_SUPABASE_URL || '', 5 | process.env.SUPABASE_SERVICE_ROLE_KEY || '' 6 | ); 7 | 8 | export default supabaseAdmin; -------------------------------------------------------------------------------- /components/ui/LoadingDots/LoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import s from './LoadingDots.module.css'; 2 | 3 | const LoadingDots = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default LoadingDots; 14 | -------------------------------------------------------------------------------- /utils/prompts.ts: -------------------------------------------------------------------------------- 1 | import { HNStory, HNStoryCurated } from "types"; 2 | 3 | export function generatePromptToSummarize( 4 | story: HNStoryCurated 5 | ){ 6 | return `Given this hacker news story, summarize it in 4 paragraphs, including people's comments and details.\nTitle: ${story.title}\nBody: ${story.text}\nTop Comments: ${story.comments}\n` 7 | } -------------------------------------------------------------------------------- /utils/hacker_news_api.ts: -------------------------------------------------------------------------------- 1 | import { HNStory } from "types" 2 | 3 | export function fetchHN(id: string) : Promise{ 4 | const url = `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty` 5 | return fetch(url) 6 | .then((res) => res.json()) 7 | .then((data) => { 8 | return data 9 | }) 10 | } -------------------------------------------------------------------------------- /styles/chrome-bug.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Chrome has a bug with transitions on load since 2012! 3 | * 4 | * To prevent a "pop" of content, you have to disable all transitions until 5 | * the page is done loading. 6 | * 7 | * https://lab.laukstein.com/bug/input 8 | * https://twitter.com/timer150/status/1345217126680899584 9 | */ 10 | body.loading * { 11 | transition: none !important; 12 | } 13 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | ); 14 | } 15 | } 16 | 17 | export default MyDocument; 18 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | env:{ 5 | POSTHOG_KEY: process.env.POSTHOG_KEY, 6 | OPENAI_API_KEY: process.env.OPENAI_API_KEY, 7 | NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, 8 | NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 9 | SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY, 10 | } 11 | 12 | } 13 | 14 | module.exports = nextConfig 15 | 16 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export interface PageMeta { 2 | title: string; 3 | description: string; 4 | cardImage: string; 5 | } 6 | 7 | 8 | // create a type script type based on json above 9 | export interface HNStory { 10 | by: string; 11 | descendants: number; 12 | id: number; 13 | kids: number[] | HNStory[]; 14 | kidsResult: string[]; 15 | score: number; 16 | text?: string; 17 | time: number; 18 | title: string; 19 | type: string; 20 | } 21 | 22 | export interface HNStoryCurated { 23 | comments: string; 24 | text: string; 25 | title: string; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /utils/ai_wrapper.ts: -------------------------------------------------------------------------------- 1 | import { ChatGPTAPI, getOpenAIAuth } from 'chatgpt' 2 | import { HNStory, HNStoryCurated } from 'types'; 3 | import { generatePromptToSummarize } from './prompts'; 4 | 5 | export async function getResponseFromAI(prompt: string) { 6 | return await fetch("api/ai/gpt3", { 7 | method: "POST", 8 | headers: { 9 | "Content-Type": "application/json" 10 | }, 11 | body: JSON.stringify({ 12 | prompt: prompt, 13 | }) 14 | }).then(res => res.json()).then(data => { 15 | return data.data; 16 | }) 17 | 18 | } -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # editors 37 | .vscode 38 | 39 | node_modules 40 | .next 41 | /.next 42 | .next/ -------------------------------------------------------------------------------- /components/ui/LoadingDots/LoadingDots.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply inline-flex text-center items-center leading-7; 3 | } 4 | 5 | .root span { 6 | @apply bg-zinc-200 rounded-full h-2 w-2; 7 | animation-name: blink; 8 | animation-duration: 1.4s; 9 | animation-iteration-count: infinite; 10 | animation-fill-mode: both; 11 | margin: 0 2px; 12 | } 13 | 14 | .root span:nth-of-type(2) { 15 | animation-delay: 0.2s; 16 | } 17 | 18 | .root span:nth-of-type(3) { 19 | animation-delay: 0.4s; 20 | } 21 | 22 | @keyframes blink { 23 | 0% { 24 | opacity: 0.2; 25 | } 26 | 20% { 27 | opacity: 1; 28 | } 29 | 100% { 30 | opacity: 0.2; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/ui/Card.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | title: string; 5 | description?: string | ReactNode; 6 | footer?: ReactNode; 7 | children: ReactNode; 8 | } 9 | 10 | 11 | export default function Card({ title, description, footer, children }: Props) { 12 | return ( 13 |
14 |
15 |

{title}

16 |

{description}

17 | {children} 18 |
19 |
20 | {footer} 21 |
22 |
23 | ); 24 | } -------------------------------------------------------------------------------- /utils/api.ts: -------------------------------------------------------------------------------- 1 | export async function getBaseCompletion(prompt: string, mendableToken="", completeSenteces = true, ) { 2 | // fetch our server, post request with prompt 3 | return fetch("api/ai/getMendableCompletion", 4 | { 5 | method: "POST", 6 | headers: { 7 | "Content-Type": "application/json", 8 | }, 9 | body: JSON.stringify({ 10 | prompt: prompt, 11 | mendableToken: mendableToken 12 | }) 13 | } 14 | 15 | ) 16 | .then((response) => { 17 | return response.json(); 18 | }) 19 | .then((data: any) => { 20 | if (data.success) { 21 | return data.data; 22 | } 23 | return "Error: " + data.error; 24 | }) 25 | .catch((err) => { 26 | console.log(err); 27 | }); 28 | 29 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "baseUrl": ".", 21 | "paths": { 22 | "@/components/*": ["components/*"], 23 | "@/utils/*": ["utils/*"], 24 | "@/styles/*": ["styles/*"] 25 | }, 26 | "incremental": true 27 | }, 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /utils/analytics.ts: -------------------------------------------------------------------------------- 1 | 2 | import posthog from 'posthog-js' 3 | 4 | 5 | class AnalyticsTracking { 6 | // Hi 7 | private production = process.env.NODE_ENV !== "development" ?? true; 8 | private static _instance: AnalyticsTracking; 9 | 10 | private constructor() { 11 | //... 12 | } 13 | 14 | public static get Instance() { 15 | // Do you need arguments? Make it a regular static method instead. 16 | return this._instance || (this._instance = new this()); 17 | } 18 | 19 | init() { 20 | posthog.init(process.env.POSTHOG_KEY ?? "", { api_host: 'https://app.posthog.com' }) 21 | } 22 | 23 | track(text: string, data:Object = {}) { 24 | if (this.production) { 25 | 26 | posthog.capture(text, data); 27 | 28 | } 29 | } 30 | 31 | } 32 | 33 | export const analyticsInstance = AnalyticsTracking.Instance; -------------------------------------------------------------------------------- /components/ui/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply bg-hnorange text-white cursor-pointer inline-flex px-10 rounded-sm leading-6 transition ease-in-out duration-150 shadow-sm font-semibold text-center justify-center uppercase py-4 border border-transparent items-center; 3 | } 4 | 5 | .root:hover { 6 | @apply bg-hnorange bg-opacity-75 text-white border border-gray-50; 7 | } 8 | 9 | .root:focus { 10 | @apply outline-none ring-2 ring-pink-500 ring-opacity-50; 11 | } 12 | 13 | .root[data-active] { 14 | @apply bg-zinc-600; 15 | } 16 | 17 | .loading { 18 | @apply bg-zinc-700 text-zinc-500 border-zinc-600 cursor-not-allowed; 19 | } 20 | 21 | .slim { 22 | @apply py-2 transform-none normal-case; 23 | } 24 | 25 | .disabled, 26 | .disabled:hover { 27 | @apply text-zinc-400 border-zinc-600 bg-zinc-700 cursor-not-allowed; 28 | filter: grayscale(1); 29 | -webkit-transform: translateZ(0); 30 | -webkit-perspective: 1000; 31 | -webkit-backface-visibility: hidden; 32 | } 33 | -------------------------------------------------------------------------------- /components/ui/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes, ChangeEvent } from 'react'; 2 | import cn from 'classnames'; 3 | import s from './Input.module.css'; 4 | 5 | interface Props extends Omit, 'onChange'> { 6 | className?: string; 7 | onChange: (value: string) => void; 8 | } 9 | const Input = (props: Props) => { 10 | const { className, children, onChange, ...rest } = props; 11 | 12 | const rootClassName = cn(s.root, {}, className); 13 | 14 | const handleOnChange = (e: ChangeEvent) => { 15 | if (onChange) { 16 | onChange(e.target.value); 17 | } 18 | return null; 19 | }; 20 | 21 | return ( 22 | 33 | ); 34 | }; 35 | 36 | export default Input; 37 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Hero from '@/components/ui/Hero/Hero'; 2 | import supabaseAdmin from '@/utils/supabase_admin'; 3 | import RecentSumarries from "../components/ui/RecentSummaries"; 4 | 5 | interface Props { 6 | } 7 | 8 | export default function Index({ data }: { data: [] }) { 9 | return <> 10 | {/* Add a column to the left of the hero, without affecting hero center, that displays the most recents summaries */} 11 |
12 | 13 | 14 |
15 | 16 | 17 | } 18 | 19 | 20 | // get server side props 21 | export async function getServerSideProps(context: any) { 22 | // get the last 10 summaries from the 'hn-artciles' table 23 | const { data, error } = await supabaseAdmin 24 | .from('hn-articles') 25 | .select('*') 26 | .order('created_at', { ascending: false }) 27 | .limit(10); 28 | 29 | return { 30 | props: { 31 | data: data 32 | } 33 | } 34 | } 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /components/ui/CodeWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AiOutlineCopy } from 'react-icons/ai' 3 | import 'highlight.js/styles/monokai-sublime.css'; 4 | 5 | export default function CodeWrapper({ code, renderWithHighlight, border=true }: { code: string, renderWithHighlight?: boolean, border?: boolean }) { 6 | return ( 7 |
 9 |             
{ 10 | navigator.clipboard.writeText(code) 11 | }}> 12 | { 13 | navigator.clipboard.writeText(code) 14 | }} /> 15 |
16 | {renderWithHighlight === true ? 17 | 21 | :{code} {" "}} 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | 2 | import 'styles/main.css'; 3 | import 'styles/chrome-bug.css'; 4 | import { useEffect, useState } from 'react'; 5 | import React from 'react'; 6 | 7 | import Layout from 'components/Layout'; 8 | import { AppProps } from 'next/app'; 9 | import { Analytics } from '@vercel/analytics/react'; 10 | import { usePostHog } from 'next-use-posthog'; 11 | 12 | import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' 13 | import { SessionContextProvider } from '@supabase/auth-helpers-react' 14 | 15 | export default function MyApp({ Component, pageProps }: AppProps) { 16 | const [supabase] = useState(() => createBrowserSupabaseClient()) 17 | 18 | useEffect(() => { 19 | document.body.classList?.remove('loading'); 20 | }, []); 21 | 22 | usePostHog(process.env.POSTHOG_KEY ?? "", { 23 | api_host: 'https://app.posthog.com', loaded: (posthog) => { 24 | if (process.env.NODE_ENV === 'development') posthog.opt_out_capturing() 25 | }, 26 | }) 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/ui/RecentSummaries.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react' 3 | 4 | export default function RecentSummaries({data}: {data: any}) { 5 | return ( 6 |
7 |

Recent summaries (Global):

8 |
9 | {data.map((item: any) => { 10 | return 20 | })} 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/icons/GitHub.tsx: -------------------------------------------------------------------------------- 1 | const GitHub = ({ ...props }) => { 2 | return ( 3 | 10 | 16 | 17 | ); 18 | }; 19 | 20 | export default GitHub; 21 | -------------------------------------------------------------------------------- /styles/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | *, 6 | *:before, 7 | *:after { 8 | box-sizing: inherit; 9 | } 10 | 11 | *:focus { 12 | @apply outline-none ring-1 ring-hnorange ring-opacity-50; 13 | } 14 | 15 | @font-face { 16 | font-family: "Manrope"; 17 | src: url("../public/fonts/Manrope/Manrope-Medium.ttf"); 18 | } 19 | 20 | html { 21 | height: 100%; 22 | box-sizing: border-box; 23 | touch-action: manipulation; 24 | font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0; 25 | } 26 | 27 | html, 28 | body { 29 | font-family: "Manrope", -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue', 30 | 'Helvetica', sans-serif; 31 | text-rendering: optimizeLegibility; 32 | -moz-osx-font-smoothing: grayscale; 33 | @apply text-black bg-white antialiased; 34 | } 35 | 36 | body { 37 | position: relative; 38 | min-height: 100%; 39 | margin: 0; 40 | } 41 | 42 | a { 43 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 44 | } 45 | 46 | .animated { 47 | -webkit-animation-duration: 1s; 48 | animation-duration: 1s; 49 | -webkit-animation-duration: 1s; 50 | animation-duration: 1s; 51 | -webkit-animation-fill-mode: both; 52 | animation-fill-mode: both; 53 | } 54 | 55 | .height-screen-helper { 56 | height: calc(100vh - 80px); 57 | } 58 | 59 | .box-shadow { 60 | filter: drop-shadow(0px 40px 75px rgba(105, 42, 215, .5)); 61 | } 62 | .box-shadow-soft { 63 | filter: drop-shadow(0px 40px 75px rgba(105, 42, 215, .3)); 64 | } -------------------------------------------------------------------------------- /pages/[id].tsx: -------------------------------------------------------------------------------- 1 | import Hero from '@/components/ui/Hero/Hero' 2 | import supabaseAdmin from '@/utils/supabase_admin' 3 | import React from 'react' 4 | import RecentSumarries from "../components/ui/RecentSummaries"; 5 | 6 | export default function Id({ id, summary, data }: { id: string , summary: string, data: []}) { 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } 14 | 15 | // get the id from url 16 | export async function getServerSideProps(context: any) { 17 | const { id } = context.query 18 | // query supbase database hn articles to see if there is a summary 19 | // if there is a summary, return the summary 20 | const supabaseServer =await supabaseAdmin.from('hn-articles').select('*').eq('item', id); 21 | // get the last 10 summaries from the 'hn-artciles' table 22 | const { data, error } = await supabaseAdmin 23 | .from('hn-articles') 24 | .select('*') 25 | .order('created_at', { ascending: false }) 26 | .limit(10); 27 | if (supabaseServer.data && supabaseServer.data.length > 0) { 28 | return { 29 | props: { 30 | id, 31 | summary: supabaseServer.data[0].summary, 32 | data: data 33 | } 34 | } 35 | } 36 | return { 37 | props: { 38 | id, 39 | summary: "", 40 | data: data 41 | 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /components/ui/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import React, { forwardRef, useRef, ButtonHTMLAttributes } from 'react'; 3 | import mergeRefs from 'react-merge-refs'; 4 | import styles from './Button.module.css'; 5 | 6 | import LoadingDots from 'components/ui/LoadingDots'; 7 | 8 | interface Props extends ButtonHTMLAttributes { 9 | variant?: 'slim' | 'flat'; 10 | active?: boolean; 11 | width?: number; 12 | loading?: boolean; 13 | Component?: React.ComponentType; 14 | } 15 | 16 | const Button = forwardRef((props, buttonRef) => { 17 | const { 18 | className, 19 | variant = 'flat', 20 | children, 21 | active, 22 | width, 23 | loading = false, 24 | disabled = false, 25 | style = {}, 26 | Component = 'button', 27 | ...rest 28 | } = props; 29 | const ref = useRef(null); 30 | const rootClassName = cn( 31 | styles.root, 32 | { 33 | [styles.slim]: variant === 'slim', 34 | [styles.loading]: loading, 35 | [styles.disabled]: disabled 36 | }, 37 | className 38 | ); 39 | return ( 40 | 52 | {children} 53 | {loading && ( 54 | 55 | 56 | 57 | )} 58 | 59 | ); 60 | }); 61 | 62 | export default Button; 63 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { useRouter } from 'next/router'; 3 | import Footer from 'components/ui/Footer'; 4 | import { ReactNode } from 'react'; 5 | import { PageMeta } from '../types'; 6 | 7 | interface Props { 8 | children: ReactNode; 9 | meta?: PageMeta; 10 | } 11 | 12 | export default function Layout({ children, meta: pageMeta }: Props) { 13 | const router = useRouter(); 14 | const meta = { 15 | title: 'Hacker News Summarizer', 16 | description: 'Brought to you by SideGuide + GPT-3', 17 | // cardImage: '/og.png', 18 | ...pageMeta 19 | }; 20 | 21 | return ( 22 | <> 23 | 24 | {meta.title} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {/* */} 34 | 35 | 36 | 37 | 38 | {/* */} 39 | 40 |
{children}
41 |