├── supabase
└── .gitignore
├── components
├── ui
│ ├── Avatar
│ │ ├── index.ts
│ │ └── Avatar.tsx
│ ├── Brand
│ │ └── index.ts
│ ├── Button
│ │ ├── index.ts
│ │ └── Button.tsx
│ ├── Footer
│ │ └── index.ts
│ ├── Input
│ │ ├── index.tsx
│ │ └── Input.tsx
│ ├── Label
│ │ ├── index.ts
│ │ └── Label.tsx
│ ├── Link
│ │ ├── index.ts
│ │ └── LinkItem.tsx
│ ├── Navbar
│ │ ├── index.ts
│ │ └── ButtonMenu.tsx
│ ├── Radio
│ │ ├── index.ts
│ │ └── Radio.tsx
│ ├── Banner
│ │ ├── index.ts
│ │ └── Banner.tsx
│ ├── Checkbox
│ │ ├── index.ts
│ │ └── Checkbox.tsx
│ ├── Page404
│ │ ├── index.ts
│ │ └── Page404.tsx
│ ├── Textarea
│ │ ├── index.ts
│ │ └── Textarea.tsx
│ ├── UploadAvatar
│ │ ├── index.ts
│ │ └── UploadAvatar.tsx
│ ├── AvatarMenu
│ │ ├── index.ts
│ │ └── AvatarMenu.tsx
│ ├── LabelError
│ │ ├── index.ts
│ │ └── LabelError.tsx
│ ├── LinkShiny
│ │ ├── index.ts
│ │ └── LinkShiny.tsx
│ ├── ButtonUpvote
│ │ └── index.ts
│ ├── CategoryInput
│ │ └── index.ts
│ ├── LogoUploader
│ │ ├── index.ts
│ │ └── LogoUploader.tsx
│ ├── ToolCardList
│ │ ├── index.ts
│ │ └── ToolCardList.tsx
│ ├── BlurBackground
│ │ ├── index.ts
│ │ └── BlurBackground.tsx
│ ├── CommandPalette
│ │ ├── index.ts
│ │ ├── EmptyState.tsx
│ │ └── SearchItem.tsx
│ ├── SelectmenuDate
│ │ ├── index.ts
│ │ └── SelectmenuDate.tsx
│ ├── TabsLink
│ │ ├── index.ts
│ │ └── Tabs.tsx
│ ├── ToolCardEffect
│ │ ├── index.ts
│ │ └── ToolCardEffect.tsx
│ ├── UserProfileInfo
│ │ ├── index.ts
│ │ └── UserProfileInfo.tsx
│ ├── TagsGroup
│ │ ├── index.ts
│ │ ├── TagsGroup.tsx
│ │ └── Tag.tsx
│ ├── FormLaunch
│ │ ├── index.tsx
│ │ ├── FormLaunchWrapper.tsx
│ │ └── FormLaunchSection.tsx
│ ├── ImagesUploader
│ │ ├── index.ts
│ │ ├── ImageUploaderItem.tsx
│ │ └── ImagesUploader.tsx
│ ├── Stats
│ │ ├── index.ts
│ │ ├── Stat.tsx
│ │ ├── Stats.Wrapper.tsx
│ │ ├── Stat.CountItem.tsx
│ │ └── Stat.Item.tsx
│ ├── Gallery
│ │ ├── index.ts
│ │ ├── VideoThumbnail.tsx
│ │ ├── GalleryImage.tsx
│ │ └── ButtonHandler.tsx
│ ├── Comment
│ │ ├── Comment.Deleted.tsx
│ │ ├── Comment.Date.tsx
│ │ ├── CommentFrom.Wrapper.tsx
│ │ ├── Comment.Context.tsx
│ │ ├── Comment.UserName.tsx
│ │ ├── Comments.tsx
│ │ ├── Comment.Form.tsx
│ │ ├── index.ts
│ │ ├── Comment.tsx
│ │ ├── Comment.Textarea.tsx
│ │ ├── Comment.UserAvatar.tsx
│ │ ├── Comment.Like.tsx
│ │ └── Comment.ActionMenu.tsx
│ ├── ToolCard
│ │ ├── Tool.Title.tsx
│ │ ├── Tool.Footer.tsx
│ │ ├── Tool.views.tsx
│ │ ├── Tool.Tags.tsx
│ │ ├── Tool.Logo.tsx
│ │ ├── Tool.Name.tsx
│ │ ├── ToolCardLink.tsx
│ │ └── ToolCard.tsx
│ ├── HighlightCode
│ │ └── index.tsx
│ ├── ProfileFormModal
│ │ └── index.tsx
│ ├── ModalBannerCode
│ │ └── ModalBannerCodeClient.tsx
│ ├── Client
│ │ ├── CommentsSection.tsx
│ │ ├── CommentSection.tsx
│ │ └── CommentFormSection.tsx
│ ├── ProductHuntCard
│ │ └── index.tsx
│ ├── PaymentForm.tsx
│ ├── Blog
│ │ ├── Pagination.tsx
│ │ └── ArticleCard.tsx
│ ├── AuthProviderButtons
│ │ └── index.tsx
│ ├── Skeletons
│ │ └── SkeletonToolCard.tsx
│ ├── ChatWindow
│ │ └── index.tsx
│ ├── Alert
│ │ └── index.tsx
│ ├── WinnerBadge
│ │ └── index.tsx
│ ├── Modal
│ │ └── index.tsx
│ ├── MonitizerAdCards
│ │ └── index.tsx
│ ├── LoginPage
│ │ └── index.tsx
│ ├── SelectLaunchDate
│ │ └── index.tsx
│ ├── ToolViewModal
│ │ └── TrendingToolsList.tsx
│ └── TrendingToolsList
│ │ └── index.tsx
├── PaymentFormScript.tsx
├── Icons
│ ├── IconPlay.tsx
│ ├── IconPlus.tsx
│ ├── IconChevronLeft.tsx
│ ├── IconChevronRight.tsx
│ ├── IconArrowLongRight.tsx
│ ├── IconXmark.tsx
│ ├── IconCodeBracket.tsx
│ ├── IconSearch.tsx
│ ├── IconArrowLongLeft.tsx
│ ├── IconClipboard.tsx
│ ├── IconEllipsisVertical.tsx
│ ├── IconInformationCircle.tsx
│ ├── IconEye.tsx
│ ├── IconLoading.tsx
│ ├── IconChatBubbleOvalLeftEllipsis.tsx
│ ├── IconHeart.tsx
│ ├── IconPencilSquare.tsx
│ ├── IconChatBubbleLeft.tsx
│ ├── IconPhoto.tsx
│ ├── IconChartBar.tsx
│ ├── IconFire.tsx
│ ├── IconTrash.tsx
│ ├── IconArrowTopRight.tsx
│ ├── IconGlobeAlt.tsx
│ ├── IconGoogle.tsx
│ ├── index.ts
│ ├── IconCalendar.tsx
│ ├── IconVote.tsx
│ └── IconNewsletterEnvolpe.tsx
├── Protectedroute.tsx
├── supabase
│ ├── provider.tsx
│ └── listener.tsx
└── CodeBlock.tsx
├── public
├── 6f2ec52fc3a4faced95247e7e7230602.txt
├── devhuntog.png
├── johnrush.jpeg
├── devhuntog-1.png
├── vercel.svg
├── next.svg
└── user.svg
├── app
├── favicon.ico
├── robots.txt
├── api
│ ├── test
│ │ └── route.ts
│ ├── newsletter
│ │ └── route.ts
│ ├── login
│ │ └── route.ts
│ └── ph-dev-tools
│ │ ├── route.ts
│ │ ├── [slug]
│ │ └── route.ts
│ │ └── get-website-url
│ │ └── [url]
│ │ └── route.ts
├── login
│ └── page.tsx
├── account
│ ├── layout.tsx
│ └── tools
│ │ └── edit
│ │ └── layout.tsx
├── auth
│ └── callback
│ │ └── route.ts
├── blog
│ ├── sitemap.xml
│ │ └── route.tsx
│ ├── page.tsx
│ ├── tag
│ │ └── [slug]
│ │ │ └── page.tsx
│ └── category
│ │ └── [slug]
│ │ └── page.tsx
├── globals.css
├── all-dev-tools
│ └── page.tsx
├── oss-friends
│ └── page.tsx
├── the-story
│ └── stats.jsx
├── prismjs-theme.css
├── sitemap.xml
│ └── route.tsx
├── upcoming
│ └── page.tsx
└── tools
│ └── [slug]
│ └── page.tsx
├── postcss.config.js
├── utils
├── mergeTW.ts
├── supabase
│ ├── services
│ │ ├── BaseDbService.ts
│ │ ├── supabaseClient.ts
│ │ ├── pricing-types.ts
│ │ ├── CacheService.ts
│ │ ├── users.ts
│ │ ├── upvoteCommenLogs.ts
│ │ ├── categories.ts
│ │ ├── api.ts
│ │ └── awards.ts
│ ├── server.ts
│ ├── browser.ts
│ ├── CustomTypes.ts
│ └── fileUploader.ts
├── validateURL.ts
├── createSlug.ts
├── usermaven
│ └── index.ts
├── extractVideoId.ts
├── handleURLQuery.tsx
├── sendWelcomeEmail.ts
├── customDateFromNow.ts
├── addHttpsToUrl.ts
├── helpers.ts
└── categories.ts
├── .prettierignore
├── type.ts
├── devhunt.code-workspace
├── .prettierrc
├── pages
└── api
│ ├── auth-token.ts
│ ├── api-formatters.ts
│ ├── past-week-tools.ts
│ ├── week-tools.ts
│ └── chat-gpt.ts
├── .gitignore
├── tsconfig.json
├── middleware.ts
├── .eslintrc.json
├── LICENSE
├── tailwind.config.js
├── next.config.js
├── CONTRIBUTING.md
└── package.json
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
--------------------------------------------------------------------------------
/components/ui/Avatar/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Avatar'
2 |
--------------------------------------------------------------------------------
/components/ui/Brand/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Brand'
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/Input/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './Input'
2 |
--------------------------------------------------------------------------------
/components/ui/Label/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Label'
2 |
--------------------------------------------------------------------------------
/components/ui/Link/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './LinkItem'
2 |
--------------------------------------------------------------------------------
/components/ui/Navbar/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Navbar'
2 |
--------------------------------------------------------------------------------
/components/ui/Radio/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Radio'
2 |
--------------------------------------------------------------------------------
/components/ui/Banner/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Banner';
2 |
--------------------------------------------------------------------------------
/components/ui/Checkbox/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Checkbox'
2 |
--------------------------------------------------------------------------------
/components/ui/Page404/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Page404'
2 |
--------------------------------------------------------------------------------
/components/ui/Textarea/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Textarea'
2 |
--------------------------------------------------------------------------------
/components/ui/UploadAvatar/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from "./UploadAvatar"
--------------------------------------------------------------------------------
/components/ui/AvatarMenu/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './AvatarMenu'
2 |
--------------------------------------------------------------------------------
/components/ui/LabelError/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './LabelError'
2 |
--------------------------------------------------------------------------------
/components/ui/LinkShiny/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './LinkShiny'
2 |
--------------------------------------------------------------------------------
/public/6f2ec52fc3a4faced95247e7e7230602.txt:
--------------------------------------------------------------------------------
1 | 6f2ec52fc3a4faced95247e7e7230602
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MarsX-dev/devhunt/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/components/ui/ButtonUpvote/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ButtonUpvote'
2 |
--------------------------------------------------------------------------------
/components/ui/CategoryInput/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './CategoryInput'
2 |
--------------------------------------------------------------------------------
/components/ui/LogoUploader/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './LogoUploader';
2 |
--------------------------------------------------------------------------------
/components/ui/ToolCardList/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ToolCardList'
2 |
--------------------------------------------------------------------------------
/components/ui/BlurBackground/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './BlurBackground'
2 |
--------------------------------------------------------------------------------
/components/ui/CommandPalette/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './CommandPalette'
2 |
--------------------------------------------------------------------------------
/components/ui/SelectmenuDate/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './SelectmenuDate';
2 |
--------------------------------------------------------------------------------
/components/ui/TabsLink/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Tabs'
2 | export * from './TabLink'
3 |
--------------------------------------------------------------------------------
/components/ui/ToolCardEffect/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ToolCardEffect';
2 |
--------------------------------------------------------------------------------
/components/ui/UserProfileInfo/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './UserProfileInfo'
2 |
--------------------------------------------------------------------------------
/components/ui/TagsGroup/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TagsGroup'
2 | export * from './Tag'
3 |
--------------------------------------------------------------------------------
/public/devhuntog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MarsX-dev/devhunt/HEAD/public/devhuntog.png
--------------------------------------------------------------------------------
/public/johnrush.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MarsX-dev/devhunt/HEAD/public/johnrush.jpeg
--------------------------------------------------------------------------------
/public/devhuntog-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MarsX-dev/devhunt/HEAD/public/devhuntog-1.png
--------------------------------------------------------------------------------
/app/robots.txt:
--------------------------------------------------------------------------------
1 | User-Agent: *
2 | Allow: /
3 | Disallow: /private/
4 | Sitemap: https://devhunt.org/sitemap.xml
--------------------------------------------------------------------------------
/components/ui/FormLaunch/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './FormLaunchSection'
2 | export * from './FormLaunchWrapper'
3 |
--------------------------------------------------------------------------------
/components/ui/ImagesUploader/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ImagesUploader'
2 | export * from './ImageUploaderItem'
3 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/utils/mergeTW.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge';
2 |
3 | export default (...ClassNameValue: string[]) => twMerge(ClassNameValue);
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .serverless
2 | __generated__
3 | CHANGELOG.md
4 | coverage
5 | dist
6 | pnpm-lock.yaml
7 | prisma/migrations
8 | **/translations/*.json
9 |
--------------------------------------------------------------------------------
/components/ui/Stats/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Stats.Wrapper'
2 | export * from './Stat.Item'
3 | export * from './Stat.CountItem'
4 | export * from './Stat'
5 |
--------------------------------------------------------------------------------
/components/ui/Gallery/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Gallery';
2 | export * from './GalleryImage';
3 | export * from './ButtonHandler';
4 | export * from './VideoThumbnail';
5 |
--------------------------------------------------------------------------------
/app/api/test/route.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { NextResponse } from 'next/server';
3 |
4 | export async function GET() {
5 | return NextResponse.json({ data: 'We are testing something out' });
6 | }
7 |
--------------------------------------------------------------------------------
/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | import LoginPage from '@/components/ui/LoginPage';
2 |
3 | export const metadata = {
4 | title: 'Login to your account',
5 | };
6 |
7 | export default function Login() {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/type.ts:
--------------------------------------------------------------------------------
1 | import { Product } from './utils/supabase/types';
2 |
3 | export interface ProductType extends Product {
4 | product_pricing_types: {
5 | title: string;
6 | };
7 | product_categories: {
8 | name: string;
9 | }[];
10 | }
11 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.Deleted.tsx:
--------------------------------------------------------------------------------
1 | export const CommentDeleted = () => (
2 |
3 | This comment has been deleted.
4 |
5 | )
6 |
--------------------------------------------------------------------------------
/utils/supabase/services/BaseDbService.ts:
--------------------------------------------------------------------------------
1 | import { type SupabaseClient } from '@supabase/supabase-js'
2 | import { type Database } from '../types'
3 |
4 | export default abstract class BaseDbService {
5 | constructor (public supabase: SupabaseClient) {}
6 | }
7 |
--------------------------------------------------------------------------------
/utils/validateURL.ts:
--------------------------------------------------------------------------------
1 | export default function validateURL(url: string) {
2 | // Regular expression pattern for URL validation
3 | var pattern = /^(https?:\/\/)?([a-z0-9-]+\.)+[a-z]{2,}(\/.*)*$/i
4 |
5 | // Test the URL against the pattern
6 | return pattern.test(url)
7 | }
8 |
--------------------------------------------------------------------------------
/components/ui/ToolCard/Tool.Title.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import { ReactNode } from 'react';
3 |
4 | export default ({ className, children }: { className?: string; children?: ReactNode }) => (
5 | {children}
6 | );
7 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.Date.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { ReactNode } from 'react'
3 |
4 | export const CommentDate = ({ className, children }: { className?: string; children?: ReactNode }) => (
5 | {children}
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/LabelError/LabelError.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { ReactNode } from 'react'
3 |
4 | export default ({ className, children }: { className?: string; children?: ReactNode }) => (
5 | {children}
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/Stats/Stat.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { type ReactNode } from 'react'
3 |
4 | export const Stat = ({ children, className }: { children: ReactNode; className?: string }) => (
5 | {children}
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/ToolCard/Tool.Footer.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import { ReactNode } from 'react';
3 |
4 | export default ({ className, children }: { className?: string; children?: ReactNode }) => (
5 | {children}
6 | );
7 |
--------------------------------------------------------------------------------
/components/ui/TagsGroup/TagsGroup.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { ReactNode } from 'react'
3 |
4 | export const TagsGroup = ({ children, className = '' }: { children: ReactNode; className?: string }) => (
5 |
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/Comment/CommentFrom.Wrapper.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { ReactNode } from 'react'
3 |
4 | export const CommentFormWrapper = ({ children, className }: { children: ReactNode; className?: string }) => (
5 | {children}
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/HighlightCode/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import hljs from 'highlight.js';
5 | import 'highlight.js/styles/vs2015.min.css';
6 |
7 | export default function HighlightCode() {
8 | useEffect(() => {
9 | hljs.highlightAll();
10 | }, []);
11 | return null;
12 | }
13 |
--------------------------------------------------------------------------------
/components/ui/Stats/Stats.Wrapper.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { type ReactNode } from 'react'
3 |
4 | export const StatsWrapper = ({ children, className }: { children: ReactNode; className?: string }) => (
5 |
6 | )
7 |
--------------------------------------------------------------------------------
/utils/supabase/services/supabaseClient.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@supabase/supabase-js';
2 |
3 | export const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL as string, process.env.SUPABASE_SERVICE_ROLE_KEY as string, {
4 | auth: {
5 | autoRefreshToken: false,
6 | persistSession: false,
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.Context.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { ReactNode } from 'react'
3 |
4 | export const CommentContext = ({ className, children }: { className?: string; children?: ReactNode }) => (
5 | {children}
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/Stats/Stat.CountItem.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { type ReactNode } from 'react'
3 |
4 | export const StatCountItem = ({ children, className }: { children: ReactNode; className?: string }) => (
5 | {children}
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/Stats/Stat.Item.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { type ReactNode } from 'react'
3 |
4 | export const StatItem = ({ children, className }: { children: ReactNode; className?: string }) => (
5 | {children}
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.UserName.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { ReactNode } from 'react'
3 |
4 | export const CommentUserName = ({ className, children }: { className?: string; children?: ReactNode }) => (
5 | {children}
6 | )
7 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comments.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { HTMLAttributes, ReactNode, useEffect } from 'react'
4 |
5 | interface Props extends HTMLAttributes {
6 | children: ReactNode
7 | }
8 |
9 | export const Comments = ({ children, ...props }: Props) => {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/devhunt.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "name": "project-root",
5 | "path": "./"
6 | },
7 | {
8 | "name": "supabase-functions",
9 | "path": "supabase/functions"
10 | }
11 | ],
12 | "settings": {
13 | "files.exclude": {
14 | "supabase/functions/": true
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/utils/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from 'next/headers';
2 | import { type SupabaseClient, createServerComponentClient } from '@supabase/auth-helpers-nextjs';
3 | import { type Database } from '@/utils/supabase/types';
4 |
5 | export const createServerClient = (): SupabaseClient =>
6 | createServerComponentClient({ cookies });
7 |
--------------------------------------------------------------------------------
/utils/createSlug.ts:
--------------------------------------------------------------------------------
1 | export default (title: string) => {
2 | const lowercaseTitle = title.toLowerCase();
3 | const slug = lowercaseTitle
4 | .replace(/\s+/g, '-') // Replace spaces with hyphens
5 | .replace(/[^a-z0-9-]/g, '') // Remove non-alphanumeric characters (excluding hyphens)
6 | .replace(/-{2,}/g, '-'); // Remove multiple consecutive hyphens
7 | return slug;
8 | };
9 |
--------------------------------------------------------------------------------
/utils/usermaven/index.ts:
--------------------------------------------------------------------------------
1 | import { usermavenClient, UsermavenClient } from '@usermaven/sdk-js';
2 |
3 | let usermaven: UsermavenClient | null = null;
4 | if (process.env.USER_MAVEN_KEY) {
5 | usermaven = usermavenClient({
6 | key: process.env.USER_MAVEN_KEY as string,
7 | tracking_host: 'https://events.usermaven.com',
8 | });
9 | }
10 |
11 | export default usermaven;
12 |
--------------------------------------------------------------------------------
/components/PaymentFormScript.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Script from 'next/script';
3 | import { usePathname } from 'next/navigation';
4 |
5 | export default () => {
6 | const pathname = usePathname();
7 | return pathname?.includes('/account/tools/activate-launch') ? (
8 |
9 | ) : (
10 | <>>
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/components/ui/ToolCard/Tool.views.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export default ({ className, count }: { className?: string; count: number }) => (
4 |
5 |
6 | {count}
7 | Impressions
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/components/Icons/IconPlay.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconPlay = ({ className = '' }: { className?: string }) => (
4 |
7 | );
8 |
--------------------------------------------------------------------------------
/components/ui/ToolCard/Tool.Tags.tsx:
--------------------------------------------------------------------------------
1 | export default ({ items }: { items: any[] }) => (
2 |
3 | {items.slice(0, 3).map((item, idx) => (
4 | <>
5 | {item}
6 |
7 | >
8 | ))}
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/components/ui/ProfileFormModal/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Modal from '@/components/ui/Modal';
4 | import ProfileFormModal from './ProfileFormModal';
5 |
6 | export default ({ isModalOpen }: { isModalOpen: boolean }) => {
7 | return (
8 | {}} className="max-w-2xl bg-slate-900">
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "quoteProps": "as-needed",
8 | "trailingComma": "all",
9 | "bracketSpacing": true,
10 | "arrowParens": "avoid",
11 | "endOfLine": "lf",
12 | "overrides": [
13 | {
14 | "files": "*.yml",
15 | "options": {
16 | "tabWidth": 2
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/components/Protectedroute.tsx:
--------------------------------------------------------------------------------
1 | import { useSupabase } from '@/components/supabase/provider';
2 | import { type ReactNode } from 'react';
3 | import LoginPage from './ui/LoginPage';
4 |
5 | const ProtectedRoute = ({ children }: { children: ReactNode }) => {
6 | const { session } = useSupabase();
7 | const user = session?.user;
8 | return user ? <>{children}> : ;
9 | };
10 |
11 | export default ProtectedRoute;
12 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.Form.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { HTMLAttributes, ReactNode } from 'react'
3 |
4 | interface Props extends HTMLAttributes {
5 | children: ReactNode
6 | className?: string
7 | }
8 |
9 | export const CommentForm = ({ children, className, ...props }: Props) => (
10 |
13 | )
14 |
--------------------------------------------------------------------------------
/app/account/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { type ReactNode } from 'react';
4 | import { useSupabase } from '@/components/supabase/provider';
5 | import LoginPage from '../../components/ui/LoginPage';
6 |
7 | export default ({ children }: { children: ReactNode }) => {
8 | const { session } = useSupabase();
9 | const user = session?.user;
10 | return user ? {children}
: ;
11 | };
12 |
--------------------------------------------------------------------------------
/components/ui/Avatar/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import { HTMLAttributes } from 'react';
3 |
4 | interface Props extends HTMLAttributes {
5 | src?: string;
6 | alt?: string;
7 | className?: string;
8 | }
9 |
10 | export default ({ src, className, ...props }: Props) => (
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/pages/api/auth-token.ts:
--------------------------------------------------------------------------------
1 | import {NextApiRequest, NextApiResponse} from "next";
2 |
3 | export function checkAuthToken(req: NextApiRequest, res: NextApiResponse) {
4 | if (req.headers.authorization === apiAuthToken) {
5 | return true;
6 | }
7 |
8 | res.status(403).send({message: 'Auth token is missing'});
9 | return false
10 | }
11 |
12 | export const apiAuthToken = 'YPuKUaHMTE8OWNwfM6D!!9qVvPc9AfKMJtielDMpZEPIXAbE1r=iwnZg5UI';
--------------------------------------------------------------------------------
/components/ui/Gallery/VideoThumbnail.tsx:
--------------------------------------------------------------------------------
1 | import { IconPlay } from '@/components/Icons';
2 |
3 | export default ({ src }: { src: string }) => (
4 |
5 |

6 |
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/utils/supabase/browser.ts:
--------------------------------------------------------------------------------
1 | import { type SupabaseClient, createClientComponentClient } from '@supabase/auth-helpers-nextjs';
2 | import { type Database } from '@/utils/supabase/types';
3 |
4 | export const createBrowserClient = (): SupabaseClient =>
5 | createClientComponentClient({
6 | supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
7 | supabaseKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
8 | isSingleton: true,
9 | });
10 |
--------------------------------------------------------------------------------
/components/ui/Comment/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Comment.Form'
2 | export * from './CommentFrom.Wrapper'
3 | export * from './Comment'
4 | export * from './Comment.UserAvatar'
5 | export * from './Comment.Textarea'
6 | export * from './Comments'
7 | export * from './Comment.Context'
8 | export * from './Comment.Date'
9 | export * from './Comment.UserName'
10 | export * from './Comment.Like'
11 | export * from './Comment.ActionMenu'
12 | export * from './Comment.Deleted'
13 |
--------------------------------------------------------------------------------
/components/Icons/IconPlus.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconPlus = ({ className = '' }: { className?: string }) => (
4 |
12 | )
13 |
--------------------------------------------------------------------------------
/components/ui/FormLaunch/FormLaunchWrapper.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { HTMLAttributes, ReactNode } from 'react'
3 |
4 | interface Props extends HTMLAttributes {
5 | children: ReactNode
6 | className?: string
7 | }
8 |
9 | export const FormLaunchWrapper = ({ children, className = '', ...props }: Props) => (
10 |
13 | )
14 |
--------------------------------------------------------------------------------
/components/ui/Label/Label.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { HTMLAttributes, ReactNode } from 'react'
3 |
4 | interface Props extends HTMLAttributes {
5 | children: ReactNode
6 | className?: string
7 | htmlFor?: string
8 | }
9 |
10 | export default ({ children, className, ...props }: Props) => (
11 |
14 | )
15 |
--------------------------------------------------------------------------------
/utils/extractVideoId.ts:
--------------------------------------------------------------------------------
1 | export default (url: string) => {
2 | // Regular expression pattern to match YouTube video URLs
3 | var pattern =
4 | /(?:https?:\/\/(?:www\.)?)?(?:youtu\.be\/|(?:www\.|m\.)?youtube\.com\/(?:watch\?v=|v\/|embed\/|shorts\/|playlist\?list=|user\/(?:[\w#]+\/)+))([^\s&?]+)/;
5 | var match = url.match(pattern);
6 | if (match && match[1]) {
7 | return { embed: 'https://www.youtube.com/embed/' + match[1], id: match[1] };
8 | }
9 | return null;
10 | };
11 |
--------------------------------------------------------------------------------
/components/Icons/IconChevronLeft.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconChevronLeft = ({ className = '' }: { className?: string }) => (
4 |
14 | );
15 |
--------------------------------------------------------------------------------
/components/Icons/IconChevronRight.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconChevronRight = ({ className = '' }: { className?: string }) => (
4 |
14 | );
15 |
--------------------------------------------------------------------------------
/components/Icons/IconArrowLongRight.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconArrowLongRight = ({ className = '' }: { className?: string }) => (
4 |
14 | );
15 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { ReactNode } from 'react'
3 |
4 | export const Comment = ({
5 | children,
6 | className,
7 | ...props
8 | }: {
9 | children: ReactNode
10 | className?: string
11 | id?: string
12 | }) => (
13 |
14 |
15 | {children}
16 |
17 | )
18 |
--------------------------------------------------------------------------------
/utils/supabase/services/pricing-types.ts:
--------------------------------------------------------------------------------
1 | import BaseDbService from './BaseDbService'
2 | import type {
3 | ProductPricingType,
4 | } from '@/utils/supabase/types'
5 |
6 | export default class ProductPricingTypesService extends BaseDbService {
7 | async getAll (): Promise {
8 | const { data, error } = await this.supabase.from('product_pricing_types')
9 | .select()
10 | .limit(10)
11 |
12 | if (error !== null) throw new Error(error.message)
13 | return data
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/components/Icons/IconXmark.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconXmark = ({ className = '' }: { className?: string }) => (
4 |
12 | )
13 |
--------------------------------------------------------------------------------
/utils/handleURLQuery.tsx:
--------------------------------------------------------------------------------
1 | export default function handleURLQuery(url: string) {
2 | // Check if the URL contains a query string
3 | if (url.includes('?')) {
4 | // Check if the URL already contains "ref=devhunt"
5 | if (url.includes('ref=devhunt')) {
6 | return url;
7 | } else {
8 | // Append "&ref=devhunt" to the URL
9 | return url + '&ref=devhunt';
10 | }
11 | } else {
12 | // If there is no query string, add "?ref=devhunt" to the URL
13 | return url + '?ref=devhunt';
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/utils/sendWelcomeEmail.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export default async ({ firstName, personalEMail }: { firstName: string; personalEMail: string }) => {
4 | const WELCOME_EMAIL_API_KEY = process.env.WELCOME_EMAIL_API_KEY as string;
5 | const SIGNUP_FORM_ID = process.env.SIGNUP_FORM_ID as string;
6 |
7 | const apiURL = `https://cron.ventryweather.com/webhook-devhunt.php?apikey=${WELCOME_EMAIL_API_KEY}&name=${firstName}&tag=api&email=${personalEMail}&formid=${SIGNUP_FORM_ID}`;
8 | return await axios.get(apiURL);
9 | };
10 |
--------------------------------------------------------------------------------
/components/Icons/IconCodeBracket.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconCodeBracket = ({ className = '' }: { className?: string }) => (
4 |
14 | );
15 |
--------------------------------------------------------------------------------
/utils/customDateFromNow.ts:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | export default function customDateFromNow(date: string | number) {
4 | const now = moment();
5 | const inputDate = moment(date);
6 |
7 | // Calculate the difference in days
8 | const diffDays = now.diff(inputDate, 'days');
9 |
10 | if (Math.abs(diffDays) < 7) {
11 | return inputDate.fromNow(); // Use fromNow() for differences less than a week
12 | } else {
13 | return inputDate.format('MMMM Do YYYY'); // Display regular date for anything older than a week
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/pages/api/api-formatters.ts:
--------------------------------------------------------------------------------
1 | import { ExtendedProduct } from '@/utils/supabase/CustomTypes';
2 |
3 | export function simpleToolApiDtoFormatter(t: ExtendedProduct) {
4 | return {
5 | id: t.id,
6 | email: t.email,
7 | name: t.name,
8 | description: t.description,
9 | logo_url: t.logo_url,
10 | data_added: new Date(t.created_at).toISOString().split('T')[0],
11 | launch_date: t.launch_date,
12 | votes_count: t.votes_count || '-',
13 | devhunt_link: `https://devhunt.org/tool/${t.slug}`,
14 | link: t.demo_url,
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/components/Icons/IconSearch.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconSearch = ({ className = '' }: { className?: string }) => (
4 |
18 | )
19 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.Textarea.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { HTMLAttributes } from 'react'
3 |
4 | interface Props extends HTMLAttributes {
5 | className?: string
6 | value?: string
7 | }
8 |
9 | export const CommentTextarea = ({ className = '', ...props }: Props) => (
10 |
16 | )
17 |
--------------------------------------------------------------------------------
/.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 |
30 | # vercel
31 | .vercel
32 | .env
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | .env
38 | # IDE
39 | \.idea/
40 | .vscode
41 |
--------------------------------------------------------------------------------
/components/ui/ToolCard/Tool.Logo.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | const regexPattern = /w=\d+/g;
4 | const replacement = 'w=128';
5 |
6 | export default ({ src, className, imgClassName, alt }: { src: string; className?: string; imgClassName?: string; alt?: string }) => (
7 |
8 |

14 |
15 | );
16 |
--------------------------------------------------------------------------------
/components/Icons/IconArrowLongLeft.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconArrowLongLeft = ({ className = '' }: { className?: string }) => (
4 |
16 | )
17 |
--------------------------------------------------------------------------------
/components/Icons/IconClipboard.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconClipboard = ({ className = '' }: { className?: string }) => (
4 |
18 | );
19 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/ui/Gallery/GalleryImage.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const GalleryImage = ({
4 | src = '',
5 | alt = '',
6 | className = '',
7 | imgClassName = '',
8 | ...props
9 | }: {
10 | src: string;
11 | alt?: string;
12 | className?: string;
13 | imgClassName?: string;
14 | }) => {
15 | src += '&w=750';
16 |
17 | return (
18 |
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/app/api/newsletter/route.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { NextResponse } from 'next/server';
3 |
4 | export async function POST(req: Request) {
5 | const { personalEMail } = await req.json();
6 |
7 | const NEWSLETTER_FORM_ID = process.env.NEWSLETTER_FORM_ID as string;
8 | const WELCOME_EMAIL_API_KEY = process.env.WELCOME_EMAIL_API_KEY as string;
9 |
10 | const apiURL = `https://cron.ventryweather.com/webhook-devhunt.php?apikey=${WELCOME_EMAIL_API_KEY}&tag=newsletter&email=${personalEMail}&formid=${NEWSLETTER_FORM_ID}`;
11 | const res = await axios.get(apiURL);
12 | return NextResponse.json({ data: res.data });
13 | }
14 |
--------------------------------------------------------------------------------
/components/ui/Link/LinkItem.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import Link from 'next/link';
3 | import { AnchorHTMLAttributes, ReactNode } from 'react';
4 |
5 | interface Props extends AnchorHTMLAttributes {
6 | children: ReactNode;
7 | href: string;
8 | className?: string;
9 | }
10 |
11 | export default ({ children, href, className = '', ...props }: Props) => (
12 |
17 | {children}
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/components/ui/TabsLink/Tabs.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import { ReactNode } from 'react';
3 |
4 | export const Tabs = ({
5 | children,
6 | className = '',
7 | ulClassName = '',
8 | variant,
9 | }: {
10 | children: ReactNode;
11 | className?: string;
12 | ulClassName?: string;
13 | variant?: 'vertical' | 'horizontal';
14 | }) => {
15 | return (
16 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/app/api/login/route.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { NextResponse } from 'next/server';
3 |
4 | export async function POST(req: Request) {
5 | const { firstName, personalEMail } = await req.json();
6 |
7 | const WELCOME_EMAIL_API_KEY = process.env.WELCOME_EMAIL_API_KEY as string;
8 | const SIGNUP_FORM_ID = process.env.SIGNUP_FORM_ID as string;
9 |
10 | const apiURL = `https://cron.ventryweather.com/webhook-devhunt.php?apikey=${WELCOME_EMAIL_API_KEY}&name=${firstName}&tag=api&email=${personalEMail}&formid=${SIGNUP_FORM_ID}`;
11 | const res = await axios.get(apiURL);
12 | return NextResponse.json({ data: res.data });
13 | }
14 |
--------------------------------------------------------------------------------
/components/ui/BlurBackground/BlurBackground.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export default ({
4 | className = '',
5 | isActive,
6 | setActive = () => null,
7 | ...props
8 | }: {
9 | isActive: boolean;
10 | setActive?: (bool: boolean) => void;
11 | className?: string;
12 | }) => (
13 | setActive(false)}
21 | >
22 | );
23 |
--------------------------------------------------------------------------------
/components/ui/Checkbox/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { HTMLAttributes } from 'react'
3 |
4 | interface Props extends HTMLAttributes {
5 | className?: string
6 | value?: string
7 | }
8 |
9 | export default ({ className, ...props }: Props) => (
10 |
17 | )
18 |
--------------------------------------------------------------------------------
/components/Icons/IconEllipsisVertical.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconEllipsisVertical = ({ className = '' }: { className?: string }) => (
4 |
18 | );
19 |
--------------------------------------------------------------------------------
/components/Icons/IconInformationCircle.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconInformationCircle = ({ className = '' }: { className?: string }) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/components/Icons/IconEye.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconEye = ({ className = '' }: { className?: string }) => (
4 |
12 | );
13 |
--------------------------------------------------------------------------------
/components/ui/Gallery/ButtonHandler.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import { HTMLAttributes, ReactNode } from 'react';
3 |
4 | interface Props extends HTMLAttributes {
5 | children: ReactNode;
6 | className?: string;
7 | }
8 |
9 | export const ButtonHandler = ({ children, className, ...props }: Props) => (
10 |
18 | );
19 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.UserAvatar.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import Avatar from '../Avatar/Avatar'
3 |
4 | export const CommentUserAvatar = ({
5 | src = '',
6 | alt = '',
7 | className = '',
8 | }: {
9 | src: string
10 | alt?: string
11 | className?: string
12 | }) => (
13 |
14 | {src ? (
15 |
16 | ) : (
17 |
22 | )}
23 |
24 | )
25 |
--------------------------------------------------------------------------------
/utils/supabase/services/CacheService.ts:
--------------------------------------------------------------------------------
1 | import NodeCache from 'node-cache';
2 |
3 | const stdTTL = 30;
4 | class CacheService {
5 | private _cache = new NodeCache({ stdTTL });
6 |
7 | async get(key: string, asyncFetcher: () => Promise, ttl: number = stdTTL) {
8 | const value = this._cache.get(key);
9 |
10 | if (value !== undefined && value !== null) {
11 | return value;
12 | }
13 |
14 | const newValue = await asyncFetcher();
15 |
16 | this._cache.set(key, newValue, ttl);
17 | return newValue;
18 | }
19 |
20 | async del(key: string) {
21 | this._cache.del(key);
22 | }
23 | }
24 |
25 | export const cache = new CacheService();
26 |
--------------------------------------------------------------------------------
/components/ui/ModalBannerCode/ModalBannerCodeClient.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import ModalBannerCode from '@/components/ui/ModalBannerCode';
4 | import { useState } from 'react';
5 |
6 | export default () => {
7 | const [isModalOpen, setModalOpen] = useState(false);
8 | const [toolSlug, setToolSlug] = useState('');
9 |
10 | const copyDone = () => {
11 | localStorage.removeItem('last-tool');
12 | setModalOpen(false);
13 | };
14 |
15 | return (
16 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/Icons/IconLoading.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconLoading = ({ className = '' }: { className?: string }) => (
4 |
17 | );
18 |
--------------------------------------------------------------------------------
/components/ui/TagsGroup/Tag.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import Link from 'next/link'
3 | import { ReactNode } from 'react'
4 |
5 | const customStyle =
6 | 'flex-none text-sm text-slate-400 font-medium border border-slate-700 bg-slate-800/50 rounded-full px-3 py-1'
7 | export const Tag = ({ children, href, className = '' }: { children: ReactNode; href?: string; className?: string }) => (
8 |
9 | {href ? (
10 |
11 | {children}
12 |
13 | ) : (
14 | <>{children}>
15 | )}
16 |
17 | )
18 |
--------------------------------------------------------------------------------
/components/Icons/IconChatBubbleOvalLeftEllipsis.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconChatBubbleOvalLeftEllipsis = ({ className = '' }: { className?: string }) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/components/ui/Textarea/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import { HTMLAttributes } from 'react';
3 |
4 | interface Props extends HTMLAttributes {
5 | className?: string;
6 | value?: string;
7 | validate?: {};
8 | required?: boolean;
9 | }
10 |
11 | export default ({ className, validate, ...props }: Props) => (
12 |
19 | );
20 |
--------------------------------------------------------------------------------
/components/ui/Client/CommentsSection.tsx:
--------------------------------------------------------------------------------
1 | import type { Comment as CommentType } from '@/utils/supabase/types';
2 | import CommentSingle from './CommentSingle';
3 | import { Comments } from '../Comment';
4 |
5 | interface CommentTypeProp extends CommentType {
6 | profiles: {
7 | avatar_url: string;
8 | full_name: string;
9 | username: string;
10 | };
11 | }
12 |
13 | export default ({ comments, productId }: { comments: CommentTypeProp[]; productId: string }) => {
14 | return (
15 |
16 | {comments.map((comment: CommentTypeProp, idx) => (
17 |
18 | ))}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/components/ui/Radio/Radio.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import { HTMLAttributes } from 'react';
3 |
4 | interface Props extends HTMLAttributes {
5 | className?: string;
6 | id?: string;
7 | name?: string;
8 | checked?: boolean;
9 | validate?: {};
10 | required?: boolean;
11 | value?: any;
12 | }
13 |
14 | export default ({ className = '', validate, ...props }: Props) => (
15 |
23 | );
24 |
--------------------------------------------------------------------------------
/components/Icons/IconHeart.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconHeart = ({ className = '' }: { className?: string }) => (
4 |
12 | )
13 |
--------------------------------------------------------------------------------
/components/Icons/IconPencilSquare.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconPencilSquare = ({ className = '' }: { className?: string }) => (
4 |
18 | );
19 |
--------------------------------------------------------------------------------
/app/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
2 | import { cookies } from 'next/headers';
3 | import { NextResponse } from 'next/server';
4 |
5 | import type { NextRequest } from 'next/server';
6 | import { type Database } from '@/utils/supabase/types';
7 |
8 | export async function GET(request: NextRequest) {
9 | const requestUrl = new URL(request.url);
10 | const code = requestUrl.searchParams.get('code');
11 |
12 | if (code) {
13 | const supabase = createRouteHandlerClient({ cookies });
14 | await supabase.auth.exchangeCodeForSession(code);
15 | }
16 |
17 | // URL to redirect to after sign in process completes
18 | return NextResponse.redirect(requestUrl.origin);
19 | }
20 |
--------------------------------------------------------------------------------
/components/Icons/IconChatBubbleLeft.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconChatBubbleLeft = ({ className = '' }: { className?: string }) => (
4 |
16 | )
17 |
--------------------------------------------------------------------------------
/components/ui/ProductHuntCard/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import mergeTW from '@/utils/mergeTW';
4 | import { MouseEvent, ReactNode } from 'react';
5 |
6 | export default ({ href, className, children }: { href: string; className?: string; children?: ReactNode }) => {
7 | const handleClick = (e: MouseEvent) => {
8 | window.open(href, '_blank');
9 | };
10 | return (
11 |
15 | {children}
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/components/Icons/IconPhoto.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconPhoto = ({ className = '' }: { className?: string }) => (
4 |
18 | )
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": "./",
18 | "plugins": [
19 | {
20 | "name": "next"
21 | }
22 | ],
23 | "paths": {
24 | "@/*": ["./*"]
25 | }
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/components/Icons/IconChartBar.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconChartBar = ({ className = '' }: { className?: string }) => (
4 |
12 | )
13 |
--------------------------------------------------------------------------------
/components/Icons/IconFire.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconFire = ({ className = '' }: { className?: string }) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/components/ui/Input/Input.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import { HTMLAttributes } from 'react';
3 |
4 | interface Props extends HTMLAttributes {
5 | className?: string;
6 | value?: string;
7 | type?: 'text' | 'email' | 'password';
8 | name?: string;
9 | validate?: {};
10 | required?: boolean;
11 | disabled?: boolean;
12 | }
13 |
14 | export default ({ className, required, validate, ...props }: Props) => (
15 |
22 | );
23 |
--------------------------------------------------------------------------------
/components/ui/LinkShiny/LinkShiny.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 | import Link from 'next/link';
3 | import { AnchorHTMLAttributes, ReactNode } from 'react';
4 |
5 | interface Props extends AnchorHTMLAttributes {
6 | href: string;
7 | children?: ReactNode;
8 | className?: string;
9 | }
10 | export default ({ children, href, className, ...props }: Props) => {
11 | return (
12 |
19 | {children}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/utils/supabase/CustomTypes.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type Product,
3 | type ProductCategory,
4 | type ProductPricingType,
5 | type Profile,
6 | type Database,
7 | } from '@/utils/supabase/types';
8 |
9 | export type ExtendedProduct = Product & {
10 | product_pricing_types: ProductPricingType
11 | product_categories: ProductCategory[]
12 | profiles: Profile
13 | }
14 |
15 | export type ExtendedComment = Comment & {
16 | profiles: Profile
17 | }
18 |
19 | export type WinnerOfTheDay = Database['public']['Views']['winner_of_the_day']['Row']
20 | export type WinnerOfTheWeek = Database['public']['Views']['winner_of_the_week']['Row']
21 | export type WinnerOfTheMonth = Database['public']['Views']['winner_of_the_month']['Row']
22 |
23 | export type ProductAward = Database['public']['Views']['product_awards']['Row']
24 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
2 | import { type NextRequest, NextResponse } from 'next/server';
3 |
4 | import type { Database } from '@/utils/supabase/types';
5 |
6 | export async function middleware(req: NextRequest): Promise {
7 | const res = NextResponse.next();
8 | const supabase = createMiddlewareClient({ req, res });
9 |
10 | const {
11 | data: { session },
12 | } = await supabase.auth.getSession(); // destructure the data object to obtain the session object
13 | if (session === null) return NextResponse.redirect(new URL('/login', req.nextUrl));
14 | return res;
15 | }
16 |
17 | export const config = {
18 | matcher: [
19 | // add the routes you wish the middleware to run in. You can also use regex
20 | ],
21 | };
22 |
--------------------------------------------------------------------------------
/utils/addHttpsToUrl.ts:
--------------------------------------------------------------------------------
1 | export default function addHttpsToUrl(url: string) {
2 | // Trim any leading or trailing whitespace
3 | url = url.trim();
4 |
5 | // Check if the URL already starts with 'http://' or 'https://'
6 | const pattern = /^(https?:\/\/)/i;
7 |
8 | if (pattern.test(url)) {
9 | return url;
10 | }
11 |
12 | // If URL starts with 'www.', prepend 'https://'
13 | if (url.startsWith('www.')) {
14 | return 'https://' + url;
15 | }
16 |
17 | // For cases like 'http//example.com', correct by replacing 'http//' with 'https://'
18 | const malformedProtocolPattern = /^http\/\//i;
19 | if (malformedProtocolPattern.test(url)) {
20 | url = url.replace(malformedProtocolPattern, 'https://');
21 | return url;
22 | }
23 |
24 | // If no protocol or 'www' is present, prepend 'https://'
25 | return 'https://' + url;
26 | }
27 |
--------------------------------------------------------------------------------
/components/ui/CommandPalette/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | export default () => (
2 |
23 | )
24 |
--------------------------------------------------------------------------------
/components/Icons/IconTrash.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconTrash = ({ className = '' }: { className?: string }) => (
4 |
18 | );
19 |
--------------------------------------------------------------------------------
/components/Icons/IconArrowTopRight.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconArrowTopRight = ({ className = '' }: { className?: string }) => (
4 |
21 | )
22 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": "standard-with-typescript",
7 | "overrides": [],
8 | "parserOptions": {
9 | "project": "./tsconfig.json",
10 | "ecmaVersion": "latest",
11 | "sourceType": "module"
12 | },
13 | "rules": {
14 | "@typescript-eslint/comma-dangle": "off",
15 | "@typescript-eslint/space-before-function-paren": "off",
16 | "@typescript-eslint/semi": "off",
17 | "@typescript-eslint/strict-boolean-expressions": "off",
18 | "@typescript-eslint/restrict-template-expressions": "off",
19 | "@typescript-eslint/explicit-function-return-type": "off",
20 | "@typescript-eslint/member-delimiter-style": "off",
21 | "@typescript-eslint/array-type": "off",
22 | "space-before-function-paren": "off",
23 | "func-call-spacing": ["error", "never"],
24 | "semi": ["error", "always"]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.Like.tsx:
--------------------------------------------------------------------------------
1 | import { IconHeart } from '@/components/Icons'
2 | import mergeTW from '@/utils/mergeTW'
3 | import { HTMLAttributes, ReactNode } from 'react'
4 |
5 | interface Props extends HTMLAttributes {
6 | className?: string
7 | count?: number
8 | }
9 |
10 | export const CommentLike = ({ children, className, count = 0, ...props }: Props) => (
11 |
22 | )
23 |
--------------------------------------------------------------------------------
/components/ui/PaymentForm.tsx:
--------------------------------------------------------------------------------
1 | import Modal from './Modal';
2 |
3 | export default ({ isActive, toolName, email }: { isActive: boolean; toolName: string; email?: string }) => (
4 | <>
5 |
6 |
14 |
23 |
24 | >
25 | );
26 |
--------------------------------------------------------------------------------
/components/ui/CommandPalette/SearchItem.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import ProductName from '../ToolCard/Tool.Name';
3 | import ProductTitle from '../ToolCard/Tool.Title';
4 | import ProductLogo from '../ToolCard/Tool.Logo';
5 | import { Product } from '@/utils/supabase/types';
6 |
7 | export default ({ item, ...props }: { item: Product; onClick: () => void }) => (
8 |
13 |
14 |
15 |
16 |
{item.name}
17 |
{item.slogan}
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/components/ui/FormLaunch/FormLaunchSection.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 | import { ReactNode } from 'react'
3 |
4 | export const FormLaunchSection = ({
5 | children,
6 | className = '',
7 | number,
8 | title,
9 | description,
10 | }: {
11 | children: ReactNode
12 | className?: string
13 | number: number
14 | title: string
15 | description?: string
16 | }) => (
17 |
18 |
19 | {number}
20 |
21 |
22 |
23 |
{title}
24 |
{description}
25 |
26 |
{children}
27 |
28 |
29 | )
30 |
--------------------------------------------------------------------------------
/components/Icons/IconGlobeAlt.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconGlobeAlt = ({ className = '' }: { className?: string }) => (
4 |
18 | )
19 |
--------------------------------------------------------------------------------
/components/ui/ImagesUploader/ImageUploaderItem.tsx:
--------------------------------------------------------------------------------
1 | import { IconXmark } from '@/components/Icons'
2 | import mergeTW from '@/utils/mergeTW'
3 |
4 | export const ImageUploaderItem = ({
5 | className = '',
6 | src,
7 | onRemove,
8 | }: {
9 | className?: string
10 | src: string
11 | onRemove: () => void
12 | }) => (
13 |
18 |

19 |
26 |
27 | )
28 |
--------------------------------------------------------------------------------
/components/ui/ToolCard/Tool.Name.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import mergeTW from '@/utils/mergeTW';
4 | import { ReactNode } from 'react';
5 | import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/solid';
6 | import Link from 'next/link';
7 |
8 | export default ({
9 | className,
10 | children,
11 | href,
12 | toolHref,
13 | }: {
14 | className?: string;
15 | href?: string;
16 | toolHref?: string;
17 | children?: ReactNode;
18 | }) => (
19 |
31 | );
32 |
--------------------------------------------------------------------------------
/utils/supabase/fileUploader.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { type File } from 'buffer';
3 |
4 | export default async ({ files, options }: { files: File | Blob; options?: string }) => {
5 | const formdata = new FormData();
6 |
7 | const fileName = Date.now() + (files as File).name.replaceAll(' ', '-');
8 | formdata.append('image', files as any, fileName);
9 |
10 | try {
11 | const { data } = await axios.post('https://d1gl9g4ciwvjfq.cloudfront.net/api/UploadFile', formdata);
12 | return { file: getCdnImageUrl(data.file.url, options) };
13 | } catch (err) {
14 | console.log(err);
15 | }
16 | };
17 |
18 | // Converts asset url into a imgix url to have better pages loading speed
19 | function getCdnImageUrl(url: string, options?: string) {
20 | return (
21 | url.replace(/'/g, "\\'").replace('https://marscode.s3.eu-north-1.amazonaws.com/assets/img', 'https://mars-images.imgix.net') +
22 | '?auto=compress&fit=max' +
23 | (options ? `&${options}` : '')
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/components/Icons/IconGoogle.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconGoogle = ({ className = '' }: { className?: string }) => (
4 |
14 | );
15 |
--------------------------------------------------------------------------------
/components/ui/Navbar/ButtonMenu.tsx:
--------------------------------------------------------------------------------
1 | export default ({ isActive, setActive }: { isActive: boolean; setActive: () => void }) => (
2 |
21 | );
22 |
--------------------------------------------------------------------------------
/components/Icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from './IconSearch';
2 | export * from './IconVote';
3 | export * from './IconArrowTopRight';
4 | export * from './IconChartBar';
5 | export * from './IconChatBubbleLeft';
6 | export * from './IconLoading';
7 | export * from './IconGithub';
8 | export * from './IconHeart';
9 | export * from './IconEllipsisVertical';
10 | export * from './IconArrowLongLeft';
11 | export * from './IconArrowLongRight';
12 | export * from './IconPhoto';
13 | export * from './IconXmark';
14 | export * from './IconPlus';
15 | export * from './IconPencilSquare';
16 | export * from './IconTrash';
17 | export * from './IconCalendar';
18 | export * from './IconEye';
19 | export * from './IconInformationCircle';
20 | export * from './IconFire';
21 | export * from './IconGoogle';
22 | export * from './IconChatBubbleOvalLeftEllipsis';
23 | export * from './IconPlay';
24 | export * from './IconCodeBracket';
25 | export * from './IconClipboard';
26 | export * from './IconFirstWinnerBadge';
27 | export * from './IconSecondWinnerBadge';
28 | export * from './IconThirdWinnerBadge';
29 | export * from './IconNewsletterEnvolpe';
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 MarsX
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/components/supabase/provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { Session } from '@supabase/auth-helpers-nextjs';
4 | import { createContext, useContext, useState } from 'react';
5 | import type { TypedSupabaseClient } from '@/app/layout';
6 | import { createBrowserClient } from '@/utils/supabase/browser';
7 | import { type Profile } from '@/utils/supabase/types';
8 |
9 | type MaybeSession = Session | null
10 |
11 | interface SupabaseContext {
12 | supabase: TypedSupabaseClient
13 | session: MaybeSession
14 | user: Profile
15 | }
16 |
17 | // @ts-expect-error disable args error
18 | const Context = createContext();
19 |
20 | export default function SupabaseProvider({
21 | children,
22 | session,
23 | user,
24 | }: {
25 | children: React.ReactNode
26 | session: MaybeSession
27 | user: Profile
28 | }): JSX.Element {
29 | const [supabase] = useState(() => createBrowserClient());
30 |
31 | return (
32 |
33 | <>{children}>
34 |
35 | );
36 | }
37 |
38 | export const useSupabase = (): SupabaseContext => useContext(Context);
39 |
--------------------------------------------------------------------------------
/components/ui/ToolCard/ToolCardLink.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import mergeTW from '@/utils/mergeTW';
4 | import { MouseEvent, ReactNode } from 'react';
5 | import { useRouter } from 'next/navigation';
6 | import Link from 'next/link';
7 |
8 | export default ({ href, className, children }: { href: string; className?: string; children?: ReactNode }) => {
9 | const router = useRouter();
10 |
11 | const handleClick = (e: MouseEvent) => {
12 | e.preventDefault();
13 | const targetId = (e.target as HTMLDivElement).getAttribute('id');
14 | if (targetId != 'vote-item' && targetId != 'tool-title') {
15 | setTimeout(() => document.getElementById('nprogress')?.classList.remove('hidden'), 200);
16 | router.push(href);
17 | }
18 | };
19 | return (
20 |
24 | {children}
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/components/supabase/listener.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRouter } from 'next/navigation';
4 | import { useEffect } from 'react';
5 | import { useSupabase } from './provider';
6 |
7 | // this component handles refreshing server data when the user logs in or out
8 | // this method avoids the need to pass a session down to child components
9 | // in order to re-render when the user's session changes
10 | // #elegant!
11 | export default function SupabaseListener({ serverAccessToken }: { serverAccessToken?: string }): any {
12 | const { supabase } = useSupabase();
13 | const router = useRouter();
14 |
15 | useEffect(() => {
16 | const {
17 | data: { subscription },
18 | } = supabase.auth.onAuthStateChange((event, session) => {
19 | if (session?.access_token !== serverAccessToken) {
20 | // server and client are out of sync
21 | // reload the page to fetch fresh server data
22 | // https://beta.nextjs.org/docs/data-fetching/mutating
23 | router.refresh();
24 | }
25 | });
26 |
27 | return () => {
28 | subscription.unsubscribe();
29 | };
30 | }, [serverAccessToken, router, supabase]);
31 | }
32 |
--------------------------------------------------------------------------------
/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | export function omit(obj: object, keys: string[]): object {
2 | return Object.fromEntries(Object.entries(obj).filter(([k, _]) => !keys.includes(k)));
3 | }
4 |
5 | export function groupBy(array, callbackFn) {
6 | return array.reduce((res, item) => {
7 | const key = callbackFn(item);
8 | (res[key] = res[key] || []).push(item);
9 | return res;
10 | }, {});
11 | }
12 |
13 | export function groupByWithRef(array, callbackFn, refGetter) {
14 | return array.reduce((res, item) => {
15 | const key = callbackFn(item);
16 |
17 | if (!res[key]) {
18 | res[key] = {
19 | ref: refGetter(item),
20 | items: []
21 | };
22 | }
23 |
24 | res[key].items.push(item);
25 |
26 | return res;
27 | }, {});
28 | }
29 |
30 | export function shuffleToolsBasedOnDate(tools) {
31 | if (!tools) {
32 | return [];
33 | }
34 |
35 | const _copy = Array.from(tools);
36 |
37 | for (let i = 0; i < new Date().getDate(); i++) {
38 | _copy.unshift(_copy.pop());
39 | }
40 |
41 | if (_copy.length % 2 === 0 || new Date().getDate() % 2 === 0) {
42 | _copy.reverse();
43 | }
44 |
45 | return _copy;
46 | }
47 |
--------------------------------------------------------------------------------
/components/ui/Blog/Pagination.tsx:
--------------------------------------------------------------------------------
1 | interface PaginationProps {
2 | slug: string;
3 | pageNumber: number;
4 | lastPage: number;
5 | }
6 |
7 | const Pagination: React.FC = ({ slug, pageNumber, lastPage }) => {
8 | return (
9 |
30 | );
31 | };
32 |
33 | export default Pagination;
34 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}'],
4 | theme: {
5 | extend: {
6 | backgroundImage: {
7 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
8 | 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
9 | },
10 | keyframes: {
11 | overlayShow: {
12 | from: { opacity: 0 },
13 | to: { opacity: 1 },
14 | },
15 | contentShow: {
16 | from: { opacity: 0, transform: 'translate(-50%, -48%) scale(0.96)' },
17 | to: { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
18 | },
19 | },
20 | animation: {
21 | overlayShow: 'overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
22 | contentShow: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
23 | },
24 | },
25 | },
26 | plugins: [
27 | require('@tailwindcss/typography'),
28 | require('@tailwindcss/forms')({
29 | strategy: 'class', // only generate classes
30 | }),
31 | ],
32 | };
33 |
--------------------------------------------------------------------------------
/components/Icons/IconCalendar.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW';
2 |
3 | export const IconCalendar = ({ className = '' }: { className?: string }) => (
4 |
12 | );
13 |
--------------------------------------------------------------------------------
/components/Icons/IconVote.tsx:
--------------------------------------------------------------------------------
1 | import mergeTW from '@/utils/mergeTW'
2 |
3 | export const IconVote = ({ className = '' }: { className?: string }) => (
4 |
23 | )
24 |
--------------------------------------------------------------------------------
/components/ui/AuthProviderButtons/index.tsx:
--------------------------------------------------------------------------------
1 | import { IconGithub, IconGoogle } from '@/components/Icons';
2 | import Button from '@/components/ui/Button/Button';
3 | import mergeTW from '@/utils/mergeTW';
4 | import { HTMLAttributes } from 'react';
5 |
6 | interface ProviderType extends HTMLAttributes {
7 | isLoad?: boolean;
8 | className?: string;
9 | onClick?: () => void;
10 | }
11 |
12 | export const GithubProvider = ({ isLoad, className, ...props }: ProviderType) => (
13 | }
17 | className={mergeTW(
18 | `text-sm font-medium mt-8 mx-auto flex text-slate-800 bg-slate-50 hover:bg-slate-200 active:bg-slate-100 ${className}`,
19 | )}
20 | >
21 | Continue with Github
22 |
23 | );
24 |
25 | export const GoogleProvider = ({ isLoad, className, ...props }: ProviderType) => (
26 | }
30 | className={mergeTW(
31 | `text-sm font-medium mt-2 mx-auto flex text-slate-400 border border-slate-700 bg-transparent hover:text-slate-800 hover:bg-slate-50 duration-200 ${className}`,
32 | )}
33 | >
34 | Continue with Google
35 |
36 | );
37 |
--------------------------------------------------------------------------------
/components/ui/Comment/Comment.ActionMenu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { IconEllipsisVertical } from '@/components/Icons'
4 | import { ReactNode, useEffect, useRef, useState } from 'react'
5 |
6 | export const CommentActionMenu = ({ children }: { children: ReactNode }) => {
7 | const [state, setState] = useState(false)
8 | const menuBtnRef = useRef(null)
9 |
10 | useEffect(() => {
11 | const handleDropDown = (e: MouseEvent) => {
12 | if (menuBtnRef.current && !(menuBtnRef.current as HTMLElement).contains(e.target as Node)) setState(false)
13 | }
14 | document.addEventListener('click', handleDropDown)
15 | }, [])
16 |
17 | return (
18 |
19 |
26 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/Banner/Banner.tsx:
--------------------------------------------------------------------------------
1 | import { IconArrowLongRight } from '@/components/Icons';
2 | import LinkItem from '../Link/LinkItem';
3 |
4 | export default () => {
5 | return (
6 | //
7 | //
8 | //
9 | //

10 | //
Support our project: Float UI on Product Hunt now
11 | //
12 | //
17 | //
18 | // Check it out
19 | //
20 | //
21 | //
22 | //
23 | //
24 | <>>
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/components/ui/Page404/Page404.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | export default () => {
4 | return (
5 |
6 |
7 |
8 |
404 Error
9 |
Page not found
10 |
Sorry, the page you are looking for could not be found or has been removed.
11 |
15 | Go back
16 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/account/tools/edit/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { IconLoading } from '@/components/Icons';
4 | import { useSupabase } from '@/components/supabase/provider';
5 | import Page404 from '@/components/ui/Page404/Page404';
6 | import { createBrowserClient } from '@/utils/supabase/browser';
7 | import ProductsService from '@/utils/supabase/services/products';
8 | import { useParams } from 'next/navigation';
9 | import { type ReactNode, useEffect, useState } from 'react';
10 |
11 | export default ({ children }: { children: ReactNode }) => {
12 | const { id } = useParams();
13 | const { user } = useSupabase();
14 | const browserService = createBrowserClient();
15 | const tool = new ProductsService(browserService).getById(+id);
16 | const [isLoad, setLoad] = useState(true);
17 | const [isTool, setIsTool] = useState(true);
18 |
19 | useEffect(() => {
20 | tool.then(res => {
21 | setLoad(false);
22 | if (res && res.owner_id == user.id) setIsTool(true);
23 | else setIsTool(false);
24 | });
25 | }, []);
26 |
27 | return isLoad
28 | ? (
29 |
30 |
31 |
32 | )
33 | : isTool
34 | ? (
35 | children
36 | )
37 | : (
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | eslint: {
4 | ignoreDuringBuilds: true,
5 | },
6 | typescript: {
7 | // !! WARN !!
8 | // Dangerously allow production builds to successfully complete even if
9 | // your project has type errors.
10 | // !! WARN !!
11 | ignoreBuildErrors: true,
12 | },
13 | env: {
14 | DISCOR_TOOL_WEBHOOK: process.env.DISCOR_TOOL_WEBHOOK,
15 | DISCORD_USER_WEBHOOK: process.env.DISCORD_USER_WEBHOOK,
16 | USER_MAVEN_KEY: process.env.USER_MAVEN_KEY,
17 | WELCOME_EMAIL_API_KEY: process.env.WELCOME_EMAIL_API_KEY,
18 | SIGNUP_FORM_ID: process.env.SIGNUP_FORM_ID,
19 | NEWSLETTER_FORM_ID: process.env.NEWSLETTER_FORM_ID,
20 | AUTH_TOKEN_PASSWORD: process.env.AUTH_TOKEN_PASSWORD,
21 | AUTH_TOKEN_API_KEY: process.env.AUTH_TOKEN_API_KEY,
22 | CRON_SECRET: process.env.CRON_SECRET,
23 | PH_ACCESS_TOKEN: process.env.PH_ACCESS_TOKEN,
24 | },
25 | images: {
26 | remotePatterns: [
27 | {
28 | protocol: 'https',
29 | hostname: 'mars-images.imgix.net',
30 | port: '',
31 | pathname: '/seobot/devhunt.org/**',
32 | },
33 | {
34 | protocol: 'https',
35 | hostname: 'ph-files.imgix.net',
36 | port: '',
37 | },
38 | ],
39 | },
40 | };
41 | module.exports = nextConfig;
42 |
--------------------------------------------------------------------------------
/components/ui/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import { IconLoading } from '@/components/Icons';
2 | import mergeTW from '@/utils/mergeTW';
3 | import { HTMLAttributes, ReactNode, RefObject } from 'react';
4 |
5 | interface Props extends HTMLAttributes {
6 | children: ReactNode;
7 | child?: ReactNode;
8 | className?: string;
9 | isLoad?: boolean;
10 | variant?: 'shiny' | 'default';
11 | type?: 'button' | 'submit' | 'reset';
12 | ref?: RefObject;
13 | }
14 |
15 | export default ({ children, child, className = '', variant = 'default', isLoad = false, ...props }: Props) => {
16 | const variants = {
17 | shiny:
18 | 'py-2.5 px-3 font-medium text-center text-white active:shadow-none rounded-lg shadow bg-slate-700 md:bg-[linear-gradient(179.23deg,_#1E293B_0.66%,_rgba(30,_41,_59,_0)_255.99%)] hover:bg-slate-800 duration-150',
19 | default: 'py-2.5 px-3 rounded-lg duration-150 font-medium text-center text-sm text-white bg-orange-500',
20 | };
21 |
22 | return (
23 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/pages/api/past-week-tools.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import ProductsService from '@/utils/supabase/services/products';
3 | import { createBrowserClient } from '@/utils/supabase/browser';
4 | import { simpleToolApiDtoFormatter } from '@/pages/api/api-formatters';
5 | import { cache } from '@/utils/supabase/services/CacheService';
6 | import ApiService from '@/utils/supabase/services/api';
7 |
8 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
9 | let limit = parseInt((req.query.limit as string) || '2');
10 | if (limit < 1) limit = 2;
11 |
12 | const today = new Date();
13 | const productService = new ProductsService(createBrowserClient());
14 | const currentWeek = await productService.getWeekNumber(today, 2) - 1;
15 |
16 | const tools = await cache.get(
17 | `past-week-tools-api-${today.getFullYear()}-${currentWeek}-${limit}`,
18 | async () => {
19 | return await productService.getPrevLaunchWeeks(today.getFullYear(), 2, currentWeek, limit);
20 | },
21 | 60,
22 | );
23 |
24 | const result = tools.map(i => ({
25 | ...i,
26 | products: i.products.map(simpleToolApiDtoFormatter),
27 | }));
28 |
29 | const apiService = new ApiService();
30 | await apiService.insertLog({ type: 'past-week-tools', data: JSON.stringify({ today, currentWeek, tools: result }) });
31 |
32 | res.json(result);
33 | }
34 |
--------------------------------------------------------------------------------
/app/api/ph-dev-tools/route.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { NextResponse } from 'next/server';
3 | import request from 'request';
4 |
5 | export async function GET() {
6 | const today = new Date();
7 | const oneWeekAgo = new Date(today);
8 | oneWeekAgo.setDate(today.getDate() - 7);
9 |
10 | const PH_ACCESS_TOKEN = process.env.PH_ACCESS_TOKEN;
11 |
12 | const config = {
13 | headers: {
14 | Authorization: `Bearer ${PH_ACCESS_TOKEN}`,
15 | 'Content-Type': 'application/json',
16 | Accept: 'application/json',
17 | },
18 | };
19 |
20 | const body = {
21 | query: `query { posts(order: VOTES, topic: "developer-tools", postedAfter: "${new Date(oneWeekAgo).toISOString()}") {
22 | edges{
23 | cursor
24 | node{
25 | id
26 | name
27 | description
28 | url
29 | slug
30 | tagline
31 | votesCount
32 | website
33 | productLinks {
34 | url
35 | }
36 | thumbnail {
37 | url
38 | }
39 | }
40 | }
41 | }
42 | }`,
43 | };
44 |
45 | const {
46 | data: {
47 | data: {
48 | posts: { edges },
49 | },
50 | },
51 | } = await axios.post('https://api.producthunt.com/v2/api/graphql', body, config);
52 | return NextResponse.json({ posts: edges.slice(0, 10) });
53 | }
54 |
--------------------------------------------------------------------------------
/components/ui/Client/CommentSection.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useState } from 'react';
4 | import CommentFormSection from './CommentFormSection';
5 | import CommentsSection from './CommentsSection';
6 | import type { Comment as CommentType, Product } from '@/utils/supabase/types';
7 | import { useSupabase } from '@/components/supabase/provider';
8 |
9 | interface CommentTypeProp extends CommentType {
10 | profiles: {
11 | avatar_url: string;
12 | full_name: string;
13 | };
14 | }
15 |
16 | export default ({ comments, slug, productId }: { comments: CommentTypeProp[]; slug: string; productId: string }) => {
17 | const { user } = useSupabase();
18 | const [commentsCollection, setCommentsCollection] = useState(comments);
19 | useEffect(() => {
20 | setCommentsCollection(comments);
21 | }, [comments]);
22 |
23 | return (
24 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/pages/api/week-tools.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import ApiService from '@/utils/supabase/services/api';
3 | import { simpleToolApiDtoFormatter } from '@/pages/api/api-formatters';
4 |
5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
6 | let { week, limit = 3, year = new Date().getFullYear(), key } = req.query;
7 | if (key !== process.env.API_KEY) {
8 | throw new Error('Forbidden');
9 | }
10 |
11 | let weekNumber = +week;
12 | if (week && Number.isNaN(weekNumber)) {
13 | return res.status(400).json({ message: 'Please provide week number as number' });
14 | }
15 |
16 | const apiService = new ApiService();
17 |
18 | const today = new Date();
19 | const currentWeek = await apiService.getWeekNumber(today, 2);
20 |
21 | if (weekNumber === -1) {
22 | weekNumber = currentWeek > 1 ? currentWeek - 1 : 52;
23 | year = currentWeek > 1 ? today.getFullYear() : today.getFullYear() - 1;
24 | }
25 |
26 | if (!weekNumber) {
27 | weekNumber = currentWeek;
28 | }
29 |
30 | const weeks = await apiService.getPrevLaunchWeeks(year, 2, weekNumber, 1);
31 | if (!weeks || weeks.length === 0) {
32 | return [];
33 | }
34 |
35 | const { products } = weeks[0];
36 | const tools = products.slice(0, limit);
37 |
38 | const result = tools.map(simpleToolApiDtoFormatter);
39 | await apiService.insertLog({ type: 'week-tools', data: JSON.stringify({ today, weekNumber, currentWeek, limit, tools: result }) });
40 |
41 | res.json(result);
42 | }
43 |
--------------------------------------------------------------------------------
/components/ui/Skeletons/SkeletonToolCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Logo from '@/components/ui/ToolCard/Tool.Logo';
4 | import Name from '@/components/ui/ToolCard/Tool.Name';
5 | import Tags from '@/components/ui/ToolCard/Tool.Tags';
6 | import Title from '@/components/ui/ToolCard/Tool.Title';
7 | import Votes from '@/components/ui/ToolCard/Tool.Votes';
8 | import ToolFooter from '@/components/ui/ToolCard/Tool.Footer';
9 | import ToolViews from '@/components/ui/ToolCard/Tool.views';
10 |
11 | export default () => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/utils/supabase/services/users.ts:
--------------------------------------------------------------------------------
1 | import type { Profile } from '@/utils/supabase/types';
2 |
3 |
4 | import BaseDbService from './BaseDbService';
5 |
6 | interface ProfileWithEmail extends Partial {
7 | email: string;
8 | }
9 |
10 | interface UserWithEmail {
11 | email: string;
12 | id: string
13 | }
14 |
15 | export default class UsersService extends BaseDbService {
16 | async getUserWithEmails(userIds: string[]): Promise
Comments, support and feedback
26 |