├── .deepsource.toml ├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── app ├── api │ ├── public-script │ │ └── route.js │ └── support-email │ │ └── route.ts ├── auth │ └── callback │ │ └── route.ts ├── dashboard │ ├── [slug] │ │ ├── loading.tsx │ │ └── page.tsx │ └── page.tsx ├── features │ └── page.tsx ├── globals.css ├── layout.tsx ├── login │ ├── page.tsx │ └── submit-button.tsx ├── logout │ └── page.tsx ├── not-found.tsx ├── page.tsx ├── privacy │ └── page.tsx ├── reset-password │ └── page.tsx ├── signup │ ├── page.tsx │ └── submit-button.tsx ├── styles.css ├── support │ ├── SupportForm.tsx │ └── page.tsx └── terms │ └── page.tsx ├── components.json ├── components ├── AnalyticsPage.tsx ├── AuthButton.tsx ├── BrowsersCard.tsx ├── CountryCard.tsx ├── DemoSection.tsx ├── DeviceCard.tsx ├── DevicesCard.tsx ├── FAQSection.tsx ├── Footer.tsx ├── ForgotPassword.tsx ├── HeroSection.tsx ├── Navbar.tsx ├── NewSiteDialog.tsx ├── OSCard.tsx ├── OpenSource.tsx ├── PathsCard.tsx ├── PrivacyFeatures.tsx ├── PublicURLSwitch.tsx ├── ReferrersCard.tsx ├── ResetPasswordForm.tsx ├── SitesList.tsx ├── Stack.tsx ├── Steps.tsx ├── ViewsBarChart.tsx ├── features.tsx └── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input-otp.tsx │ ├── input.tsx │ ├── label.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── lib └── utils.ts ├── middleware.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── icons │ ├── nextjs.svg │ ├── resend.svg │ ├── shadcnui.svg │ ├── supabase.svg │ ├── supatycs.png │ ├── tremor.jpeg │ └── vercel.svg ├── illustrations │ └── dashboard-empty.svg ├── og.png ├── screenshots │ ├── dashboard-screenshot.png │ └── supabase.png └── track.js ├── tailwind.config.ts ├── tsconfig.json └── utils └── supabase ├── client.ts ├── middleware.ts └── server.ts /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "javascript" 5 | 6 | [analyzers.meta] 7 | plugins = ["react"] 8 | environment = [ 9 | "browser", 10 | "nodejs" 11 | ] 12 | 13 | [[transformers]] 14 | name = "prettier" -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # Name of your website 3 | NEXT_PUBLIC_SITE_NAME="Woyage" 4 | 5 | # URL of your website 6 | NEXT_PUBLIC_SITE_URL="https://woyage.app/" 7 | 8 | # Stage of your website (development/production) used to determine the environment 9 | NEXT_PUBLIC_STAGE="development" 10 | 11 | # Get this from your Supabase project settings 12 | NEXT_PUBLIC_SUPABASE_ANON_KEY="..." 13 | 14 | # Get this from your Supabase project settings 15 | NEXT_PUBLIC_SUPABASE_URL="https://project_id.supabase.co" 16 | 17 | # We are using Resend for everything related to emails, you need to sign up on Resend and set up a domain over there & setup the Supabase integration. Check out the following links for more information: 18 | # https://resend.com/docs/dashboard/domains/introduction 19 | # https://supabase.com/partners/integrations/resend 20 | # https://resend.com/api-keys 21 | 22 | # After setting up the domain, set the RESEND_EMAIL variable 23 | RESEND_EMAIL="re_..." 24 | 25 | # Support email for your website, remove this if you don't plan to implement support 26 | NEXT_PUBLIC_SUPPORT_EMAIL="support@woyage.app" 27 | 28 | # Woyage use Supabase Webhooks to email the admin whenever a new support request is created. You can set a webhook in your Supabase project settings: 29 | # https://supabase.com/docs/guides/database/webhooks 30 | 31 | # When you create a webhook, create a HEADER with a key as `supabase-verified` and a value that will be used to verify it, and set the following environment variable with the value you've set, in order to prevent unauthorized requests to your webhook 32 | SUPABASE_WEBHOOK_HEADER_KEY="...." 33 | 34 | # Email of the recipient of the support requests, remove this if you don't plan to implement support 35 | SUPPORT_RECIPIENT="hey@ishaanbedi.com" -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | ./package.lock.json 9 | package.lock.json 10 | 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ishaan Bedi 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 | -------------------------------------------------------------------------------- /app/api/public-script/route.js: -------------------------------------------------------------------------------- 1 | import { UAParser } from "ua-parser-js"; 2 | import { createServerClient } from "@supabase/ssr"; 3 | import { cookies } from "next/headers"; 4 | import { NextResponse } from "next/server"; 5 | export async function POST(request) { 6 | const cookieStore = cookies(); 7 | const supabase = createServerClient( 8 | process.env.NEXT_PUBLIC_SUPABASE_URL, 9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 10 | { 11 | cookies: { 12 | get(name) { 13 | return cookieStore.get(name)?.value; 14 | }, 15 | }, 16 | }, 17 | ); 18 | const data = await request.json(); 19 | if (!data.userAgents || !data.country || !data.id || !data.domain) { 20 | return NextResponse.json({ error: "Missing data" }, { status: 400 }); 21 | } 22 | if (data.userAgents.length === 0) { 23 | return NextResponse.json({ error: "No user agents" }, { status: 400 }); 24 | } 25 | const { userAgents, country, id } = data; 26 | const ua = UAParser(userAgents); 27 | const browser = ua.browser.name; 28 | const os = ua.os.name; 29 | const device = ua.device.type || "desktop"; 30 | const { data: website } = await supabase 31 | .from("site_domains") 32 | .select("*") 33 | .eq("domain_name", data.domain); 34 | if (!website || website.length === 0) { 35 | return NextResponse.json({ error: "Website not found" }, { status: 404 }); 36 | } 37 | var found = false; 38 | for (let i = 0; i < website.length; i++) { 39 | if (website[i].website_id === id) { 40 | found = true; 41 | break; 42 | } 43 | } 44 | if (!found) { 45 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 46 | } 47 | const { error } = await supabase.from("analytics").insert({ 48 | id: id, 49 | path: data.path, 50 | browser: browser, 51 | referrer: data.referrer, 52 | os: os, 53 | device: device, 54 | country: country, 55 | website_id: data.id, 56 | domain: data.domain, 57 | language: data.language, 58 | }); 59 | if (error) { 60 | console.error("error adding analytics:", error); 61 | return NextResponse.json(error, { status: 500 }); 62 | } 63 | return NextResponse.json(data, { status: 200 }); 64 | } 65 | -------------------------------------------------------------------------------- /app/api/support-email/route.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | const resend = new Resend(process.env.RESEND_EMAIL); 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { headers } from "next/headers"; 5 | 6 | export async function POST(request: NextRequest) { 7 | const data = await request.json(); 8 | const headersList = headers(); 9 | const key = headersList.get("supabase-verified"); 10 | if (key !== process.env.SUPABASE_WEBHOOK_HEADER_KEY) { 11 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 12 | } 13 | try { 14 | const html = ` 15 |
Name: ${data.record.name}
17 |Email: ${data.record.email}
18 |Query Type: ${data.record.query_type}
19 |Message: ${data.record.message}
20 | `; 21 | await resend.emails.send({ 22 | from: `${process.env.NEXT_PUBLIC_SUPPORT_EMAIL}`, 23 | to: `${process.env.SUPPORT_RECIPIENT}`, 24 | subject: "New Support Request", 25 | html: html, 26 | }); 27 | } catch (error) { 28 | console.error(error); 29 | return NextResponse.json({ message: "Error sending email" }); 30 | } 31 | return NextResponse.json({ message: "Email sent" }); 32 | } 33 | -------------------------------------------------------------------------------- /app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(request: Request) { 5 | const requestUrl = new URL(request.url); 6 | const code = requestUrl.searchParams.get("code"); 7 | const origin = requestUrl.origin; 8 | 9 | if (code) { 10 | const supabase = createClient(); 11 | await supabase.auth.exchangeCodeForSession(code); 12 | } 13 | 14 | return NextResponse.redirect(`${origin}/`); 15 | } 16 | -------------------------------------------------------------------------------- /app/dashboard/[slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderCircle } from "lucide-react"; 2 | const Loading = async () => { 3 | return ( 4 |37 | Possible reasons could be that the site does not exist or you do not 38 | have access to it. 39 |
40 | 41 | 42 | 43 |52 | Looks like you do not have access to this site. 53 |
54 |54 | Welcome back! Sign in to continue. 55 |
56 |Sorry, we couldn't find that page.
18 |58 | Create your account, it's free and only takes a minute. 59 |
60 |16 | Please note that {appName} is a project created by indie developers and 17 | is not affiliated in any way with any other company or service. 18 |
19 |20 | Welcome to {appName}! These terms govern your use of {appName} and any 21 | related services provided by {appName}. 22 |
23 |25 | By accessing or using {appName}, you agree to be bound by these terms of 26 | service. If you do not agree to these terms, please do not use 27 | {appName}. 28 |
29 |31 | {appName} provides analytics services for websites and applications, 32 | allowing users to track and analyze traffic to their websites. 33 |
34 |36 | Your use of {appName} is also subject to our Privacy Policy, which 37 | explains how we collect, use, and disclose your information. Please 38 | review our Privacy Policy carefully. 39 |
40 |42 | We reserve the right to modify these terms of service at any time. 43 | Changes will be effective immediately upon posting to the website. Your 44 | continued use of {appName} after any such changes constitutes your 45 | acceptance of the new terms. 46 |
47 |49 | {appName} and its developers and contributors will not be liable for any 50 | direct, indirect, incidental, special, consequential, or exemplary 51 | damages, including but not limited to, damages for loss of profits, 52 | goodwill, use, data, or other intangible losses resulting from the use 53 | of or inability to use {appName}. 54 |
55 |57 | You agree to indemnify and hold harmless {appName} and its developers 58 | and contributors from and against any and all claims and expenses, 59 | including attorneys' fees, arising out of your use of {appName}, 60 | including but not limited to your violation of these terms of service. 61 |
62 |64 | We may disclose your information to government or law enforcement 65 | officials if we believe it is necessary to comply with a legal 66 | requirement, protect the rights, property, or safety of {appName} or 67 | others, or prevent illegal activity. 68 |
69 |
71 | Our service is intended for users aged 13 and older. We do not knowingly
72 | collect personal information from anyone under 13 years old.
73 |
74 | If you find out that a child under 13 has provided us with any personal information,
75 | please contact us, and we will take steps to remove such information and
76 | terminate the child's account.
77 |
80 | We reserve the right to terminate or suspend your account and access 81 | to {appName} at any time, with or without notice. A valid reason for such a 82 | termination would be provided. 83 |
84 |86 | A breach of terms would be considered if you use {appName} for any 87 | illegal or unauthorized purpose, or violate any laws in your 88 | jurisdiction. 89 |
90 |
92 | {appName} is a free service and does not require any payment. However,
93 | we reserve the right to introduce paid features in the future.
94 |
95 | As of {lastUpdated}, {appName} is a free service and does not require
96 | any payment at all.
97 |
100 | All content on {appName}, including but not limited to text, graphics, 101 | logos, icons, images, audio clips, digital downloads, data compilations, 102 | and software, is either owned by {appName} or sourced from the public 103 | domain. I have made every effort to ensure that all content on this 104 | website is either original or sourced from the public domain. If you 105 | believe that any content on this website infringes on your copyright, 106 | please reach out to me and I will be happy to address. 107 |
108 |
110 | Phew, that was a lot of legal jargon! Obviously, that's not my forte.
111 |
I am an indie developer and I created {appName} as a side project.
112 | Please let me know if something more needs to be added to this in order
113 | to make it more clear and safe and secure for everyone.
114 |
118 | If you have any questions about these terms of service, please contact 119 | me at hey@ishaanbedi.com 120 |
121 |14 | Have a look at this quick 1-minute demo video to see how{" "} 15 | {process.env.NEXT_PUBLIC_SITE_NAME} works. 16 |
17 |31 | Devices 32 |
33 |54 | {faq.question} 55 |
56 |{faq.answer}
59 |38 | {process.env.NEXT_PUBLIC_SITE_NAME} is a free and open-source 39 | analytics tool that helps you track your website traffic. 40 |
41 |31 | {process.env.NEXT_PUBLIC_SITE_NAME} is an open source project. You 32 | can contribute to the project by submitting issues, feature 33 | requests, or pull requests on GitHub. 34 |
35 |55 | Paths 56 |
57 |72 | When you use {process.env.NEXT_PUBLIC_SITE_NAME}, you can trust 73 | that your data is safe, and information about your visitors is 74 | kept private. 75 |
76 |98 | {feature.description} 99 |
100 |52 | Referrers 53 |
54 |88 | {process.env.NEXT_PUBLIC_SITE_NAME} is powered by the following 89 | technologies, that makes it super fast, efficient and reliable. 90 |
91 |{item.description}
105 | 106 |128 | {step.description} 129 |
130 |100 | An overview of all the core features{" "} 101 | {process.env.NEXT_PUBLIC_SITE_NAME} provides. 102 |
103 |129 | {feature.description} 130 |
131 |162 | {body} 163 |
164 | ); 165 | }); 166 | FormMessage.displayName = "FormMessage"; 167 | 168 | export { 169 | useFormField, 170 | Form, 171 | FormItem, 172 | FormLabel, 173 | FormControl, 174 | FormDescription, 175 | FormMessage, 176 | FormField, 177 | }; 178 | -------------------------------------------------------------------------------- /components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const HoverCard = HoverCardPrimitive.Root 9 | 10 | const HoverCardTrigger = HoverCardPrimitive.Trigger 11 | 12 | const HoverCardContent = React.forwardRef< 13 | React.ElementRef