feature(public,docs): new public website and docs
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
import { Navbar } from '../navbar';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Layout({ children }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Navbar darkText />
|
||||
<div
|
||||
className="absolute left-0 right-0 top-0 z-0 h-screen w-full bg-[radial-gradient(circle_at_2px_2px,#D9DEF6_2px,transparent_0)] text-blue-950 opacity-50"
|
||||
style={{
|
||||
backgroundSize: '70px 70px',
|
||||
}}
|
||||
/>
|
||||
<div className="relative">{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,525 +0,0 @@
|
||||
import { Heading1 } from '../../copy';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="container mt-[150px] max-w-2xl">
|
||||
<article className="prose">
|
||||
<Heading1>Privacy Policy</Heading1>
|
||||
<p>Last updated: February 22, 2024</p>
|
||||
<p>
|
||||
This Privacy Policy describes Our policies and procedures on the
|
||||
collection, use and disclosure of Your information when You use the
|
||||
Service and tells You about Your privacy rights and how the law
|
||||
protects You.
|
||||
</p>
|
||||
<p>
|
||||
We use Your Personal data to provide and improve the Service. By using
|
||||
the Service, You agree to the collection and use of information in
|
||||
accordance with this Privacy Policy.
|
||||
</p>
|
||||
<h2>Interpretation and Definitions</h2>
|
||||
<h3>Interpretation</h3>
|
||||
<p>
|
||||
The words of which the initial letter is capitalized have meanings
|
||||
defined under the following conditions. The following definitions
|
||||
shall have the same meaning regardless of whether they appear in
|
||||
singular or in plural.
|
||||
</p>
|
||||
<h3>Definitions</h3>
|
||||
<p>For the purposes of this Privacy Policy:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Account</strong> means a unique account created for You to
|
||||
access our Service or parts of our Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Affiliate</strong> means an entity that controls, is
|
||||
controlled by or is under common control with a party, where
|
||||
"control" means ownership of 50% or more of the shares,
|
||||
equity interest or other securities entitled to vote for election
|
||||
of directors or other managing authority.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Application</strong> refers to Openpanel, the software
|
||||
program provided by the Company.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Company</strong> (referred to as either "the
|
||||
Company", "We", "Us" or "Our"
|
||||
in this Agreement) refers to Coderax AB, Sankt Eriksgatan 100, 113
|
||||
31, Stockholm.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Cookies</strong> are small files that are placed on Your
|
||||
computer, mobile device or any other device by a website,
|
||||
containing the details of Your browsing history on that website
|
||||
among its many uses.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Country</strong> refers to: Sweden
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Device</strong> means any device that can access the
|
||||
Service such as a computer, a cellphone or a digital tablet.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Personal Data</strong> is any information that relates to
|
||||
an identified or identifiable individual.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Service</strong> refers to the Application or the Website
|
||||
or both.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Service Provider</strong> means any natural or legal
|
||||
person who processes the data on behalf of the Company. It refers
|
||||
to third-party companies or individuals employed by the Company to
|
||||
facilitate the Service, to provide the Service on behalf of the
|
||||
Company, to perform services related to the Service or to assist
|
||||
the Company in analyzing how the Service is used.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Usage Data</strong> refers to data collected
|
||||
automatically, either generated by the use of the Service or from
|
||||
the Service infrastructure itself (for example, the duration of a
|
||||
page visit).
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Website</strong> refers to Openpanel, accessible from{' '}
|
||||
<a
|
||||
href="https://openpanel.dev"
|
||||
rel="noreferrer external nofollow noopener"
|
||||
target="_blank"
|
||||
>
|
||||
https://openpanel.dev
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>You</strong> means the individual accessing or using the
|
||||
Service, or the company, or other legal entity on behalf of which
|
||||
such individual is accessing or using the Service, as applicable.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Collecting and Using Your Personal Data</h2>
|
||||
<h3>Types of Data Collected</h3>
|
||||
<h4>Personal Data</h4>
|
||||
<p>
|
||||
While using Our Service, We may ask You to provide Us with certain
|
||||
personally identifiable information that can be used to contact or
|
||||
identify You. Personally identifiable information may include, but is
|
||||
not limited to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Email address</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>First name and last name</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Usage Data</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h4>Usage Data</h4>
|
||||
<p>Usage Data is collected automatically when using the Service.</p>
|
||||
<p>
|
||||
Usage Data may include information such as Your Device's Internet
|
||||
Protocol address (e.g. IP address), browser type, browser version, the
|
||||
pages of our Service that You visit, the time and date of Your visit,
|
||||
the time spent on those pages, unique device identifiers and other
|
||||
diagnostic data.
|
||||
</p>
|
||||
<p>
|
||||
When You access the Service by or through a mobile device, We may
|
||||
collect certain information automatically, including, but not limited
|
||||
to, the type of mobile device You use, Your mobile device unique ID,
|
||||
the IP address of Your mobile device, Your mobile operating system,
|
||||
the type of mobile Internet browser You use, unique device identifiers
|
||||
and other diagnostic data.
|
||||
</p>
|
||||
<p>
|
||||
We may also collect information that Your browser sends whenever You
|
||||
visit our Service or when You access the Service by or through a
|
||||
mobile device.
|
||||
</p>
|
||||
<h4>Tracking Technologies and Cookies</h4>
|
||||
<p>
|
||||
We use Cookies and similar tracking technologies to track the activity
|
||||
on Our Service and store certain information. Tracking technologies
|
||||
used are beacons, tags, and scripts to collect and track information
|
||||
and to improve and analyze Our Service. The technologies We use may
|
||||
include:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Cookies or Browser Cookies.</strong> A cookie is a small
|
||||
file placed on Your Device. You can instruct Your browser to refuse
|
||||
all Cookies or to indicate when a Cookie is being sent. However, if
|
||||
You do not accept Cookies, You may not be able to use some parts of
|
||||
our Service. Unless you have adjusted Your browser setting so that
|
||||
it will refuse Cookies, our Service may use Cookies.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Web Beacons.</strong> Certain sections of our Service and
|
||||
our emails may contain small electronic files known as web beacons
|
||||
(also referred to as clear gifs, pixel tags, and single-pixel gifs)
|
||||
that permit the Company, for example, to count users who have
|
||||
visited those pages or opened an email and for other related website
|
||||
statistics (for example, recording the popularity of a certain
|
||||
section and verifying system and server integrity).
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Cookies can be "Persistent" or "Session" Cookies.
|
||||
Persistent Cookies remain on Your personal computer or mobile device
|
||||
when You go offline, while Session Cookies are deleted as soon as You
|
||||
close Your web browser. You can learn more about cookies{' '}
|
||||
<a
|
||||
href="https://www.termsfeed.com/blog/cookies/#What_Are_Cookies"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
We use both Session and Persistent Cookies for the purposes set out
|
||||
below:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Necessary / Essential Cookies</strong>
|
||||
</p>
|
||||
<p>Type: Session Cookies</p>
|
||||
<p>Administered by: Us</p>
|
||||
<p>
|
||||
Purpose: These Cookies are essential to provide You with services
|
||||
available through the Website and to enable You to use some of its
|
||||
features. They help to authenticate users and prevent fraudulent
|
||||
use of user accounts. Without these Cookies, the services that You
|
||||
have asked for cannot be provided, and We only use these Cookies
|
||||
to provide You with those services.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Cookies Policy / Notice Acceptance Cookies</strong>
|
||||
</p>
|
||||
<p>Type: Persistent Cookies</p>
|
||||
<p>Administered by: Us</p>
|
||||
<p>
|
||||
Purpose: These Cookies identify if users have accepted the use of
|
||||
cookies on the Website.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Functionality Cookies</strong>
|
||||
</p>
|
||||
<p>Type: Persistent Cookies</p>
|
||||
<p>Administered by: Us</p>
|
||||
<p>
|
||||
Purpose: These Cookies allow us to remember choices You make when
|
||||
You use the Website, such as remembering your login details or
|
||||
language preference. The purpose of these Cookies is to provide
|
||||
You with a more personal experience and to avoid You having to
|
||||
re-enter your preferences every time You use the Website.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
For more information about the cookies we use and your choices
|
||||
regarding cookies, please visit our Cookies Policy or the Cookies
|
||||
section of our Privacy Policy.
|
||||
</p>
|
||||
<h3>Use of Your Personal Data</h3>
|
||||
<p>The Company may use Personal Data for the following purposes:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To provide and maintain our Service</strong>, including to
|
||||
monitor the usage of our Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To manage Your Account:</strong> to manage Your
|
||||
registration as a user of the Service. The Personal Data You
|
||||
provide can give You access to different functionalities of the
|
||||
Service that are available to You as a registered user.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>For the performance of a contract:</strong> the
|
||||
development, compliance and undertaking of the purchase contract
|
||||
for the products, items or services You have purchased or of any
|
||||
other contract with Us through the Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To contact You:</strong> To contact You by email,
|
||||
telephone calls, SMS, or other equivalent forms of electronic
|
||||
communication, such as a mobile application's push
|
||||
notifications regarding updates or informative communications
|
||||
related to the functionalities, products or contracted services,
|
||||
including the security updates, when necessary or reasonable for
|
||||
their implementation.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To provide You</strong> with news, special offers and
|
||||
general information about other goods, services and events which
|
||||
we offer that are similar to those that you have already purchased
|
||||
or enquired about unless You have opted not to receive such
|
||||
information.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To manage Your requests:</strong> To attend and manage
|
||||
Your requests to Us.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>For business transfers:</strong> We may use Your
|
||||
information to evaluate or conduct a merger, divestiture,
|
||||
restructuring, reorganization, dissolution, or other sale or
|
||||
transfer of some or all of Our assets, whether as a going concern
|
||||
or as part of bankruptcy, liquidation, or similar proceeding, in
|
||||
which Personal Data held by Us about our Service users is among
|
||||
the assets transferred.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>For other purposes</strong>: We may use Your information
|
||||
for other purposes, such as data analysis, identifying usage
|
||||
trends, determining the effectiveness of our promotional campaigns
|
||||
and to evaluate and improve our Service, products, services,
|
||||
marketing and your experience.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
We may share Your personal information in the following situations:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>With Service Providers:</strong> We may share Your personal
|
||||
information with Service Providers to monitor and analyze the use of
|
||||
our Service, to contact You.
|
||||
</li>
|
||||
<li>
|
||||
<strong>For business transfers:</strong> We may share or transfer
|
||||
Your personal information in connection with, or during negotiations
|
||||
of, any merger, sale of Company assets, financing, or acquisition of
|
||||
all or a portion of Our business to another company.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With Affiliates:</strong> We may share Your information with
|
||||
Our affiliates, in which case we will require those affiliates to
|
||||
honor this Privacy Policy. Affiliates include Our parent company and
|
||||
any other subsidiaries, joint venture partners or other companies
|
||||
that We control or that are under common control with Us.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With business partners:</strong> We may share Your
|
||||
information with Our business partners to offer You certain
|
||||
products, services or promotions.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With other users:</strong> when You share personal
|
||||
information or otherwise interact in the public areas with other
|
||||
users, such information may be viewed by all users and may be
|
||||
publicly distributed outside.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With Your consent</strong>: We may disclose Your personal
|
||||
information for any other purpose with Your consent.
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Retention of Your Personal Data</h3>
|
||||
<p>
|
||||
The Company will retain Your Personal Data only for as long as is
|
||||
necessary for the purposes set out in this Privacy Policy. We will
|
||||
retain and use Your Personal Data to the extent necessary to comply
|
||||
with our legal obligations (for example, if we are required to retain
|
||||
your data to comply with applicable laws), resolve disputes, and
|
||||
enforce our legal agreements and policies.
|
||||
</p>
|
||||
<p>
|
||||
The Company will also retain Usage Data for internal analysis
|
||||
purposes. Usage Data is generally retained for a shorter period of
|
||||
time, except when this data is used to strengthen the security or to
|
||||
improve the functionality of Our Service, or We are legally obligated
|
||||
to retain this data for longer time periods.
|
||||
</p>
|
||||
<h3>Transfer of Your Personal Data</h3>
|
||||
<p>
|
||||
Your information, including Personal Data, is processed at the
|
||||
Company's operating offices and in any other places where the
|
||||
parties involved in the processing are located. It means that this
|
||||
information may be transferred to — and maintained on — computers
|
||||
located outside of Your state, province, country or other governmental
|
||||
jurisdiction where the data protection laws may differ than those from
|
||||
Your jurisdiction.
|
||||
</p>
|
||||
<p>
|
||||
Your consent to this Privacy Policy followed by Your submission of
|
||||
such information represents Your agreement to that transfer.
|
||||
</p>
|
||||
<p>
|
||||
The Company will take all steps reasonably necessary to ensure that
|
||||
Your data is treated securely and in accordance with this Privacy
|
||||
Policy and no transfer of Your Personal Data will take place to an
|
||||
organization or a country unless there are adequate controls in place
|
||||
including the security of Your data and other personal information.
|
||||
</p>
|
||||
<h3>Delete Your Personal Data</h3>
|
||||
<p>
|
||||
You have the right to delete or request that We assist in deleting the
|
||||
Personal Data that We have collected about You.
|
||||
</p>
|
||||
<p>
|
||||
Our Service may give You the ability to delete certain information
|
||||
about You from within the Service.
|
||||
</p>
|
||||
<p>
|
||||
You may update, amend, or delete Your information at any time by
|
||||
signing in to Your Account, if you have one, and visiting the account
|
||||
settings section that allows you to manage Your personal information.
|
||||
You may also contact Us to request access to, correct, or delete any
|
||||
personal information that You have provided to Us.
|
||||
</p>
|
||||
<p>
|
||||
Please note, however, that We may need to retain certain information
|
||||
when we have a legal obligation or lawful basis to do so.
|
||||
</p>
|
||||
<h3>Disclosure of Your Personal Data</h3>
|
||||
<h4>Business Transactions</h4>
|
||||
<p>
|
||||
If the Company is involved in a merger, acquisition or asset sale,
|
||||
Your Personal Data may be transferred. We will provide notice before
|
||||
Your Personal Data is transferred and becomes subject to a different
|
||||
Privacy Policy.
|
||||
</p>
|
||||
<h4>Law enforcement</h4>
|
||||
<p>
|
||||
Under certain circumstances, the Company may be required to disclose
|
||||
Your Personal Data if required to do so by law or in response to valid
|
||||
requests by public authorities (e.g. a court or a government agency).
|
||||
</p>
|
||||
<h4>Other legal requirements</h4>
|
||||
<p>
|
||||
The Company may disclose Your Personal Data in the good faith belief
|
||||
that such action is necessary to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Comply with a legal obligation</li>
|
||||
<li>Protect and defend the rights or property of the Company</li>
|
||||
<li>
|
||||
Prevent or investigate possible wrongdoing in connection with the
|
||||
Service
|
||||
</li>
|
||||
<li>
|
||||
Protect the personal safety of Users of the Service or the public
|
||||
</li>
|
||||
<li>Protect against legal liability</li>
|
||||
</ul>
|
||||
<h3>Security of Your Personal Data</h3>
|
||||
<p>
|
||||
The security of Your Personal Data is important to Us, but remember
|
||||
that no method of transmission over the Internet, or method of
|
||||
electronic storage is 100% secure. While We strive to use commercially
|
||||
acceptable means to protect Your Personal Data, We cannot guarantee
|
||||
its absolute security.
|
||||
</p>
|
||||
<h2>Children's Privacy</h2>
|
||||
<p>
|
||||
Our Service does not address anyone under the age of 13. We do not
|
||||
knowingly collect personally identifiable information from anyone
|
||||
under the age of 13. If You are a parent or guardian and You are aware
|
||||
that Your child has provided Us with Personal Data, please contact Us.
|
||||
If We become aware that We have collected Personal Data from anyone
|
||||
under the age of 13 without verification of parental consent, We take
|
||||
steps to remove that information from Our servers.
|
||||
</p>
|
||||
<p>
|
||||
If We need to rely on consent as a legal basis for processing Your
|
||||
information and Your country requires consent from a parent, We may
|
||||
require Your parent's consent before We collect and use that
|
||||
information.
|
||||
</p>
|
||||
<h2>Links to Other Websites</h2>
|
||||
<p>
|
||||
Our Service may contain links to other websites that are not operated
|
||||
by Us. If You click on a third party link, You will be directed to
|
||||
that third party's site. We strongly advise You to review the
|
||||
Privacy Policy of every site You visit.
|
||||
</p>
|
||||
<p>
|
||||
We have no control over and assume no responsibility for the content,
|
||||
privacy policies or practices of any third party sites or services.
|
||||
</p>
|
||||
<h2>Changes to this Privacy Policy</h2>
|
||||
<p>
|
||||
We may update Our Privacy Policy from time to time. We will notify You
|
||||
of any changes by posting the new Privacy Policy on this page.
|
||||
</p>
|
||||
<p>
|
||||
We will let You know via email and/or a prominent notice on Our
|
||||
Service, prior to the change becoming effective and update the
|
||||
"Last updated" date at the top of this Privacy Policy.
|
||||
</p>
|
||||
<p>
|
||||
You are advised to review this Privacy Policy periodically for any
|
||||
changes. Changes to this Privacy Policy are effective when they are
|
||||
posted on this page.
|
||||
</p>
|
||||
<h2>Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions about this Privacy Policy, You can contact
|
||||
us:
|
||||
</p>
|
||||
<ul>
|
||||
<li>By email: hello@openpanel.dev</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,458 +0,0 @@
|
||||
import { Heading1 } from '../../copy';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="container mt-[150px] max-w-2xl">
|
||||
<article className="prose">
|
||||
<Heading1>Terms and Conditions</Heading1>
|
||||
<p>Last updated: February 22, 2024</p>
|
||||
<p>
|
||||
Please read these terms and conditions carefully before using Our
|
||||
Service.
|
||||
</p>
|
||||
<h2>Interpretation and Definitions</h2>
|
||||
<h3>Interpretation</h3>
|
||||
<p>
|
||||
The words of which the initial letter is capitalized have meanings
|
||||
defined under the following conditions. The following definitions
|
||||
shall have the same meaning regardless of whether they appear in
|
||||
singular or in plural.
|
||||
</p>
|
||||
<h3>Definitions</h3>
|
||||
<p>For the purposes of these Terms and Conditions:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Application</strong> means the software program provided
|
||||
by the Company downloaded by You on any electronic device, named
|
||||
Openpanel
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Application Store</strong> means the digital distribution
|
||||
service operated and developed by Apple Inc. (Apple App Store) or
|
||||
Google Inc. (Google Play Store) in which the Application has been
|
||||
downloaded.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Affiliate</strong> means an entity that controls, is
|
||||
controlled by or is under common control with a party, where
|
||||
"control" means ownership of 50% or more of the shares,
|
||||
equity interest or other securities entitled to vote for election
|
||||
of directors or other managing authority.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Account</strong> means a unique account created for You to
|
||||
access our Service or parts of our Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Country</strong> refers to: Sweden
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Company</strong> (referred to as either "the
|
||||
Company", "We", "Us" or "Our"
|
||||
in this Agreement) refers to Coderax AB, Sankt Eriksgatan 100, 113
|
||||
31, Stockholm.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Device</strong> means any device that can access the
|
||||
Service such as a computer, a cellphone or a digital tablet.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Free Trial</strong> refers to a limited period of time
|
||||
that may be free when purchasing a Subscription.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Service</strong> refers to the Application or the Website
|
||||
or both.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Subscriptions</strong> refer to the services or access to
|
||||
the Service offered on a subscription basis by the Company to You.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Terms and Conditions</strong> (also referred as
|
||||
"Terms") mean these Terms and Conditions that form the
|
||||
entire agreement between You and the Company regarding the use of
|
||||
the Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Third-party Social Media Service</strong> means any
|
||||
services or content (including data, information, products or
|
||||
services) provided by a third-party that may be displayed,
|
||||
included or made available by the Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Website</strong> refers to Openpanel, accessible from{' '}
|
||||
<a
|
||||
href="https://openpanel.dev"
|
||||
rel="noreferrer external nofollow noopener"
|
||||
target="_blank"
|
||||
>
|
||||
https://openpanel.dev
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>You</strong> means the individual accessing or using the
|
||||
Service, or the company, or other legal entity on behalf of which
|
||||
such individual is accessing or using the Service, as applicable.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Acknowledgment</h2>
|
||||
<p>
|
||||
These are the Terms and Conditions governing the use of this Service
|
||||
and the agreement that operates between You and the Company. These
|
||||
Terms and Conditions set out the rights and obligations of all users
|
||||
regarding the use of the Service.
|
||||
</p>
|
||||
<p>
|
||||
Your access to and use of the Service is conditioned on Your
|
||||
acceptance of and compliance with these Terms and Conditions. These
|
||||
Terms and Conditions apply to all visitors, users and others who
|
||||
access or use the Service.
|
||||
</p>
|
||||
<p>
|
||||
By accessing or using the Service You agree to be bound by these Terms
|
||||
and Conditions. If You disagree with any part of these Terms and
|
||||
Conditions then You may not access the Service.
|
||||
</p>
|
||||
<p>
|
||||
You represent that you are over the age of 18. The Company does not
|
||||
permit those under 18 to use the Service.
|
||||
</p>
|
||||
<p>
|
||||
Your access to and use of the Service is also conditioned on Your
|
||||
acceptance of and compliance with the Privacy Policy of the Company.
|
||||
Our Privacy Policy describes Our policies and procedures on the
|
||||
collection, use and disclosure of Your personal information when You
|
||||
use the Application or the Website and tells You about Your privacy
|
||||
rights and how the law protects You. Please read Our Privacy Policy
|
||||
carefully before using Our Service.
|
||||
</p>
|
||||
<h2>Subscriptions</h2>
|
||||
<h3>Subscription period</h3>
|
||||
<p>
|
||||
The Service or some parts of the Service are available only with a
|
||||
paid Subscription. You will be billed in advance on a recurring and
|
||||
periodic basis (such as daily, weekly, monthly or annually), depending
|
||||
on the type of Subscription plan you select when purchasing the
|
||||
Subscription.
|
||||
</p>
|
||||
<p>
|
||||
At the end of each period, Your Subscription will automatically renew
|
||||
under the exact same conditions unless You cancel it or the Company
|
||||
cancels it.
|
||||
</p>
|
||||
<h3>Subscription cancellations</h3>
|
||||
<p>
|
||||
You may cancel Your Subscription renewal either through Your Account
|
||||
settings page or by contacting the Company. You will not receive a
|
||||
refund for the fees You already paid for Your current Subscription
|
||||
period and You will be able to access the Service until the end of
|
||||
Your current Subscription period.
|
||||
</p>
|
||||
<h3>Billing</h3>
|
||||
<p>
|
||||
You shall provide the Company with accurate and complete billing
|
||||
information including full name, address, state, zip code, telephone
|
||||
number, and a valid payment method information.
|
||||
</p>
|
||||
<p>
|
||||
Should automatic billing fail to occur for any reason, the Company
|
||||
will issue an electronic invoice indicating that you must proceed
|
||||
manually, within a certain deadline date, with the full payment
|
||||
corresponding to the billing period as indicated on the invoice.
|
||||
</p>
|
||||
<h3>Fee Changes</h3>
|
||||
<p>
|
||||
The Company, in its sole discretion and at any time, may modify the
|
||||
Subscription fees. Any Subscription fee change will become effective
|
||||
at the end of the then-current Subscription period.
|
||||
</p>
|
||||
<p>
|
||||
The Company will provide You with reasonable prior notice of any
|
||||
change in Subscription fees to give You an opportunity to terminate
|
||||
Your Subscription before such change becomes effective.
|
||||
</p>
|
||||
<p>
|
||||
Your continued use of the Service after the Subscription fee change
|
||||
comes into effect constitutes Your agreement to pay the modified
|
||||
Subscription fee amount.
|
||||
</p>
|
||||
<h3>Refunds</h3>
|
||||
<p>
|
||||
Except when required by law, paid Subscription fees are
|
||||
non-refundable.
|
||||
</p>
|
||||
<p>
|
||||
Certain refund requests for Subscriptions may be considered by the
|
||||
Company on a case-by-case basis and granted at the sole discretion of
|
||||
the Company.
|
||||
</p>
|
||||
<h3>Free Trial</h3>
|
||||
<p>
|
||||
The Company may, at its sole discretion, offer a Subscription with a
|
||||
Free Trial for a limited period of time.
|
||||
</p>
|
||||
<p>
|
||||
You may be required to enter Your billing information in order to sign
|
||||
up for the Free Trial.
|
||||
</p>
|
||||
<p>
|
||||
If You do enter Your billing information when signing up for a Free
|
||||
Trial, You will not be charged by the Company until the Free Trial has
|
||||
expired. On the last day of the Free Trial period, unless You canceled
|
||||
Your Subscription, You will be automatically charged the applicable
|
||||
Subscription fees for the type of Subscription You have selected.
|
||||
</p>
|
||||
<p>
|
||||
At any time and without notice, the Company reserves the right to (i)
|
||||
modify the terms and conditions of the Free Trial offer, or (ii)
|
||||
cancel such Free Trial offer.
|
||||
</p>
|
||||
<h2>User Accounts</h2>
|
||||
<p>
|
||||
When You create an account with Us, You must provide Us information
|
||||
that is accurate, complete, and current at all times. Failure to do so
|
||||
constitutes a breach of the Terms, which may result in immediate
|
||||
termination of Your account on Our Service.
|
||||
</p>
|
||||
<p>
|
||||
You are responsible for safeguarding the password that You use to
|
||||
access the Service and for any activities or actions under Your
|
||||
password, whether Your password is with Our Service or a Third-Party
|
||||
Social Media Service.
|
||||
</p>
|
||||
<p>
|
||||
You agree not to disclose Your password to any third party. You must
|
||||
notify Us immediately upon becoming aware of any breach of security or
|
||||
unauthorized use of Your account.
|
||||
</p>
|
||||
<p>
|
||||
You may not use as a username the name of another person or entity or
|
||||
that is not lawfully available for use, a name or trademark that is
|
||||
subject to any rights of another person or entity other than You
|
||||
without appropriate authorization, or a name that is otherwise
|
||||
offensive, vulgar or obscene.
|
||||
</p>
|
||||
<h2>Intellectual Property</h2>
|
||||
<p>
|
||||
The Service and its original content (excluding Content provided by
|
||||
You or other users), features and functionality are and will remain
|
||||
the exclusive property of the Company and its licensors.
|
||||
</p>
|
||||
<p>
|
||||
The Service is protected by copyright, trademark, and other laws of
|
||||
both the Country and foreign countries.
|
||||
</p>
|
||||
<p>
|
||||
Our trademarks and trade dress may not be used in connection with any
|
||||
product or service without the prior written consent of the Company.
|
||||
</p>
|
||||
<h2>Links to Other Websites</h2>
|
||||
<p>
|
||||
Our Service may contain links to third-party web sites or services
|
||||
that are not owned or controlled by the Company.
|
||||
</p>
|
||||
<p>
|
||||
The Company has no control over, and assumes no responsibility for,
|
||||
the content, privacy policies, or practices of any third party web
|
||||
sites or services. You further acknowledge and agree that the Company
|
||||
shall not be responsible or liable, directly or indirectly, for any
|
||||
damage or loss caused or alleged to be caused by or in connection with
|
||||
the use of or reliance on any such content, goods or services
|
||||
available on or through any such web sites or services.
|
||||
</p>
|
||||
<p>
|
||||
We strongly advise You to read the terms and conditions and privacy
|
||||
policies of any third-party web sites or services that You visit.
|
||||
</p>
|
||||
<h2>Termination</h2>
|
||||
<p>
|
||||
We may terminate or suspend Your Account immediately, without prior
|
||||
notice or liability, for any reason whatsoever, including without
|
||||
limitation if You breach these Terms and Conditions.
|
||||
</p>
|
||||
<p>
|
||||
Upon termination, Your right to use the Service will cease
|
||||
immediately. If You wish to terminate Your Account, You may simply
|
||||
discontinue using the Service.
|
||||
</p>
|
||||
<h2>Limitation of Liability</h2>
|
||||
<p>
|
||||
Notwithstanding any damages that You might incur, the entire liability
|
||||
of the Company and any of its suppliers under any provision of this
|
||||
Terms and Your exclusive remedy for all of the foregoing shall be
|
||||
limited to the amount actually paid by You through the Service or 100
|
||||
USD if You haven't purchased anything through the Service.
|
||||
</p>
|
||||
<p>
|
||||
To the maximum extent permitted by applicable law, in no event shall
|
||||
the Company or its suppliers be liable for any special, incidental,
|
||||
indirect, or consequential damages whatsoever (including, but not
|
||||
limited to, damages for loss of profits, loss of data or other
|
||||
information, for business interruption, for personal injury, loss of
|
||||
privacy arising out of or in any way related to the use of or
|
||||
inability to use the Service, third-party software and/or third-party
|
||||
hardware used with the Service, or otherwise in connection with any
|
||||
provision of this Terms), even if the Company or any supplier has been
|
||||
advised of the possibility of such damages and even if the remedy
|
||||
fails of its essential purpose.
|
||||
</p>
|
||||
<p>
|
||||
Some states do not allow the exclusion of implied warranties or
|
||||
limitation of liability for incidental or consequential damages, which
|
||||
means that some of the above limitations may not apply. In these
|
||||
states, each party's liability will be limited to the greatest
|
||||
extent permitted by law.
|
||||
</p>
|
||||
<h2>"AS IS" and "AS AVAILABLE" Disclaimer</h2>
|
||||
<p>
|
||||
The Service is provided to You "AS IS" and "AS
|
||||
AVAILABLE" and with all faults and defects without warranty of
|
||||
any kind. To the maximum extent permitted under applicable law, the
|
||||
Company, on its own behalf and on behalf of its Affiliates and its and
|
||||
their respective licensors and service providers, expressly disclaims
|
||||
all warranties, whether express, implied, statutory or otherwise, with
|
||||
respect to the Service, including all implied warranties of
|
||||
merchantability, fitness for a particular purpose, title and
|
||||
non-infringement, and warranties that may arise out of course of
|
||||
dealing, course of performance, usage or trade practice. Without
|
||||
limitation to the foregoing, the Company provides no warranty or
|
||||
undertaking, and makes no representation of any kind that the Service
|
||||
will meet Your requirements, achieve any intended results, be
|
||||
compatible or work with any other software, applications, systems or
|
||||
services, operate without interruption, meet any performance or
|
||||
reliability standards or be error free or that any errors or defects
|
||||
can or will be corrected.
|
||||
</p>
|
||||
<p>
|
||||
Without limiting the foregoing, neither the Company nor any of the
|
||||
company's provider makes any representation or warranty of any
|
||||
kind, express or implied: (i) as to the operation or availability of
|
||||
the Service, or the information, content, and materials or products
|
||||
included thereon; (ii) that the Service will be uninterrupted or
|
||||
error-free; (iii) as to the accuracy, reliability, or currency of any
|
||||
information or content provided through the Service; or (iv) that the
|
||||
Service, its servers, the content, or e-mails sent from or on behalf
|
||||
of the Company are free of viruses, scripts, trojan horses, worms,
|
||||
malware, timebombs or other harmful components.
|
||||
</p>
|
||||
<p>
|
||||
Some jurisdictions do not allow the exclusion of certain types of
|
||||
warranties or limitations on applicable statutory rights of a
|
||||
consumer, so some or all of the above exclusions and limitations may
|
||||
not apply to You. But in such a case the exclusions and limitations
|
||||
set forth in this section shall be applied to the greatest extent
|
||||
enforceable under applicable law.
|
||||
</p>
|
||||
<h2>Governing Law</h2>
|
||||
<p>
|
||||
The laws of the Country, excluding its conflicts of law rules, shall
|
||||
govern this Terms and Your use of the Service. Your use of the
|
||||
Application may also be subject to other local, state, national, or
|
||||
international laws.
|
||||
</p>
|
||||
<h2>Disputes Resolution</h2>
|
||||
<p>
|
||||
If You have any concern or dispute about the Service, You agree to
|
||||
first try to resolve the dispute informally by contacting the Company.
|
||||
</p>
|
||||
<h2>For European Union (EU) Users</h2>
|
||||
<p>
|
||||
If You are a European Union consumer, you will benefit from any
|
||||
mandatory provisions of the law of the country in which You are
|
||||
resident.
|
||||
</p>
|
||||
<h2>United States Federal Government End Use Provisions</h2>
|
||||
<p>
|
||||
If You are a U.S. federal government end user, our Service is a
|
||||
"Commercial Item" as that term is defined at 48 C.F.R.
|
||||
§2.101.
|
||||
</p>
|
||||
<h2>United States Legal Compliance</h2>
|
||||
<p>
|
||||
You represent and warrant that (i) You are not located in a country
|
||||
that is subject to the United States government embargo, or that has
|
||||
been designated by the United States government as a "terrorist
|
||||
supporting" country, and (ii) You are not listed on any United
|
||||
States government list of prohibited or restricted parties.
|
||||
</p>
|
||||
<h2>Severability and Waiver</h2>
|
||||
<h3>Severability</h3>
|
||||
<p>
|
||||
If any provision of these Terms is held to be unenforceable or
|
||||
invalid, such provision will be changed and interpreted to accomplish
|
||||
the objectives of such provision to the greatest extent possible under
|
||||
applicable law and the remaining provisions will continue in full
|
||||
force and effect.
|
||||
</p>
|
||||
<h3>Waiver</h3>
|
||||
<p>
|
||||
Except as provided herein, the failure to exercise a right or to
|
||||
require performance of an obligation under these Terms shall not
|
||||
affect a party's ability to exercise such right or require such
|
||||
performance at any time thereafter nor shall the waiver of a breach
|
||||
constitute a waiver of any subsequent breach.
|
||||
</p>
|
||||
<h2>Translation Interpretation</h2>
|
||||
<p>
|
||||
These Terms and Conditions may have been translated if We have made
|
||||
them available to You on our Service. You agree that the original
|
||||
English text shall prevail in the case of a dispute.
|
||||
</p>
|
||||
<h2>Changes to These Terms and Conditions</h2>
|
||||
<p>
|
||||
We reserve the right, at Our sole discretion, to modify or replace
|
||||
these Terms at any time. If a revision is material We will make
|
||||
reasonable efforts to provide at least 30 days' notice prior to
|
||||
any new terms taking effect. What constitutes a material change will
|
||||
be determined at Our sole discretion.
|
||||
</p>
|
||||
<p>
|
||||
By continuing to access or use Our Service after those revisions
|
||||
become effective, You agree to be bound by the revised terms. If You
|
||||
do not agree to the new terms, in whole or in part, please stop using
|
||||
the website and the Service.
|
||||
</p>
|
||||
<h2>Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions about these Terms and Conditions, You can
|
||||
contact us:
|
||||
</p>
|
||||
<ul>
|
||||
<li>By email: hello@openpanel.dev</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { TypeAnimation } from 'react-type-animation';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
texts: { text: string; color: string }[];
|
||||
};
|
||||
|
||||
const AnimatedText = ({ texts }: Props) => {
|
||||
const [currIndex, setCurrIndex] = useState(0);
|
||||
const sequence = useMemo(() => {
|
||||
return texts.reduce((acc, { text }, index) => {
|
||||
return [
|
||||
...acc,
|
||||
() => setCurrIndex(index),
|
||||
text,
|
||||
index === 0 ? 3000 : 2000,
|
||||
];
|
||||
}, [] as any[]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color: texts[currIndex]?.color,
|
||||
height: 60,
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<TypeAnimation
|
||||
cursor={false}
|
||||
preRenderFirstString={true}
|
||||
sequence={sequence}
|
||||
wrapper="span"
|
||||
repeat={Number.POSITIVE_INFINITY}
|
||||
omitDeletionAnimation
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedText;
|
||||
@@ -1,26 +0,0 @@
|
||||
import * as EmailValidator from 'email-validator';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { db } from '@openpanel/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const body = await req.json();
|
||||
|
||||
if (!body.email) {
|
||||
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!EmailValidator.validate(body.email)) {
|
||||
return NextResponse.json({ error: 'Email is not valid' }, { status: 400 });
|
||||
}
|
||||
|
||||
await db.waitlist.create({
|
||||
data: {
|
||||
email: String(body.email).toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(body);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Lead({ children, className }: Props) {
|
||||
return (
|
||||
<p className={cn('text-xl font-light md:text-2xl', className)}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function Lead2({ children, className }: Props) {
|
||||
return (
|
||||
<p className={cn('text-lg font-light md:text-xl', className)}>{children}</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function Paragraph({ children, className }: Props) {
|
||||
return <p className={cn('text-lg', className)}>{children}</p>;
|
||||
}
|
||||
|
||||
export function Heading1({ children, className }: Props) {
|
||||
return (
|
||||
<h1
|
||||
className={cn(
|
||||
'font-serif text-4xl font-bold !leading-tight text-slate-900 md:text-5xl',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
export function Heading2({ children, className }: Props) {
|
||||
return (
|
||||
<h2
|
||||
className={cn(
|
||||
'font-serif text-4xl font-bold text-slate-900 md:text-5xl',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
export function Heading3({ children, className }: Props) {
|
||||
return (
|
||||
<h3
|
||||
className={cn(
|
||||
'font-serif text-2xl font-bold text-slate-900 md:text-3xl',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
export function Heading4({ children, className }: Props) {
|
||||
return (
|
||||
<h3
|
||||
className={cn(
|
||||
'font-serif text-xl font-bold text-slate-900 md:text-2xl',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,145 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Heading3 } from './copy';
|
||||
|
||||
interface FeatureItem {
|
||||
title: string;
|
||||
description: string | React.ReactNode;
|
||||
className: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const features: FeatureItem[] = [
|
||||
{
|
||||
title: 'Visualize Your Data',
|
||||
description: (
|
||||
<p>
|
||||
Gain a deep understanding of your data with our visualization tools.
|
||||
</p>
|
||||
),
|
||||
className: '',
|
||||
image: '/demo-3/img-1.png',
|
||||
},
|
||||
{
|
||||
title: 'Get a good overview',
|
||||
description: (
|
||||
<p>
|
||||
Even though we want to provide advanced charts and graphs, we also want
|
||||
you to understand your data at a glance.
|
||||
</p>
|
||||
),
|
||||
className: 'bg-slate-100',
|
||||
image: '/demo-3/img-2.png',
|
||||
},
|
||||
{
|
||||
title: 'Real-Time Data Access',
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
Access all your events in real-time. No delays or waiting for data to
|
||||
be accessible.
|
||||
</p>
|
||||
<p>
|
||||
Mark events as conversions to highlight and get notifications with our
|
||||
iOS/Android app (app coming soon!)
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
className: '',
|
||||
image: '/demo-3/img-3.png',
|
||||
},
|
||||
{
|
||||
title: 'Unlimited dashboards with charts',
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
Create beautiful charts and graphs to visualize your data and share
|
||||
them with your team.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="rounded border border-border px-3 py-1">
|
||||
✅ Linear
|
||||
</div>
|
||||
<div className="rounded border border-border px-3 py-1">✅ Area</div>
|
||||
<div className="rounded border border-border px-3 py-1">✅ Bar</div>
|
||||
<div className="rounded border border-border px-3 py-1">✅ Map</div>
|
||||
<div className="rounded border border-border px-3 py-1">✅ Pie</div>
|
||||
<div className="rounded border border-border px-3 py-1">
|
||||
✅ Funnels
|
||||
</div>
|
||||
<div className="rounded border border-border px-3 py-1">
|
||||
✅ Histogram
|
||||
</div>
|
||||
<div className="rounded border border-border px-3 py-1">
|
||||
✅ Metrics
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
className: 'bg-slate-100',
|
||||
image: '/demo-3/img-4.png',
|
||||
},
|
||||
{
|
||||
title: 'Understand your users',
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
Deep dive into your user's behavior and understand how they
|
||||
interact with your app/website.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
className: '',
|
||||
image: '/demo-3/img-5.png',
|
||||
},
|
||||
];
|
||||
|
||||
export function Features() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{features.map((feature, i) => {
|
||||
return (
|
||||
<Feature key={feature.title} {...feature} even={i % 2 === 0}>
|
||||
{feature.description}
|
||||
</Feature>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Feature({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
image,
|
||||
even,
|
||||
}: FeatureItem & { even: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className={cn('group py-16', className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'container flex min-h-[300px] items-center justify-between gap-16 max-md:flex-col-reverse',
|
||||
!even && 'md:flex-row-reverse',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col">
|
||||
<Heading3 className="mb-2">{title}</Heading3>
|
||||
<div className="prose-xl">{children}</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Image
|
||||
src={image}
|
||||
alt={title}
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full max-w-xl rounded-xl border-8 border-black/5 transition-transform duration-500 group-hover:rotate-1 group-hover:scale-[101%]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Logo } from '@/components/Logo';
|
||||
import { ALink } from '@/components/ui/button';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Heading2, Lead2 } from './copy';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-blue-darker relative mt-40 text-white">
|
||||
<div className="absolute inset-0 h-full w-full bg-[radial-gradient(circle,rgba(255,255,255,0.2)_0%,rgba(255,255,255,0)_100%)]" />
|
||||
<div className="container relative flex flex-col items-center text-center">
|
||||
<div className="my-24">
|
||||
<Heading2 className="mb-2 text-white">Get early access</Heading2>
|
||||
<Lead2>
|
||||
Ready to set your analytics free? Create your account today!
|
||||
</Lead2>
|
||||
|
||||
<div className="mt-8">
|
||||
<ALink
|
||||
className="font-semibold"
|
||||
size="lg"
|
||||
href="https://dashboard.openpanel.dev"
|
||||
>
|
||||
Create your account
|
||||
</ALink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-xl">
|
||||
<div className="bg-white/20 p-2">
|
||||
<Image
|
||||
src="/demo-2/1.png"
|
||||
width={1080}
|
||||
height={608}
|
||||
alt="Openpanel overview page"
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 -mt-8">
|
||||
<div className="h-px w-full bg-[radial-gradient(circle,rgba(255,255,255,0.7)_0%,rgba(255,255,255,0.7)_50%,rgba(255,255,255,0)_100%)]" />
|
||||
<div className="bg-blue-darker p-4">
|
||||
<div className="container">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<Logo />
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<Link className="hover:underline" href="/terms">
|
||||
Terms and Conditions
|
||||
</Link>
|
||||
<Link className="hover:underline" href="/privacy">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<a
|
||||
className="hover:underline"
|
||||
href="https://x.com/OpenPanelDev"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
>
|
||||
Follow on X
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { ALink } from '@/components/ui/button';
|
||||
|
||||
import { TABLE_NAMES, chQuery } from '@openpanel/db';
|
||||
|
||||
import { cacheable } from '../../../../packages/redis';
|
||||
import AnimatedText from './animated-text';
|
||||
import { Heading1, Lead2 } from './copy';
|
||||
|
||||
function shortNumber(num: number) {
|
||||
if (num < 1e3) return num;
|
||||
if (num >= 1e3 && num < 1e6) return `${+(num / 1e3).toFixed(1)}K`;
|
||||
if (num >= 1e6 && num < 1e9) return `${+(num / 1e6).toFixed(1)}M`;
|
||||
if (num >= 1e9 && num < 1e12) return `${+(num / 1e9).toFixed(1)}B`;
|
||||
if (num >= 1e12) return `${+(num / 1e12).toFixed(1)}T`;
|
||||
}
|
||||
const getProjectsWithCount = cacheable(async function getProjectsWithCount() {
|
||||
const projects = await chQuery<{ project_id: string; count: number }>(
|
||||
`SELECT project_id, count(*) as count from ${TABLE_NAMES.events} GROUP by project_id order by count()`,
|
||||
);
|
||||
return projects;
|
||||
}, 60 * 60);
|
||||
|
||||
export async function Hero() {
|
||||
const projects = await getProjectsWithCount();
|
||||
const projectCount = projects.length;
|
||||
const eventCount = projects.reduce((acc, { count }) => acc + count, 0);
|
||||
return (
|
||||
<div className="relative overflow-hidden">
|
||||
{/* <div className="bg-blue-50 w-2/5 h-full absolute top-0 right-0"/> */}
|
||||
<div className="container relative flex sm:h-[800px] flex-col items-center gap-4 max-md:pt-32 md:flex-row md:gap-8">
|
||||
<div className="flex-1 max-md:text-center sm:min-w-[350px] lg:min-w-[400px]">
|
||||
<div className="mb-4 flex justify-center md:justify-start">
|
||||
<div className="rounded-md border border-border p-1.5 px-4 font-medium">
|
||||
FREE DURING BETA
|
||||
</div>
|
||||
</div>
|
||||
<Heading1 className="mb-4 text-slate-950">
|
||||
An open-source
|
||||
<br />
|
||||
alternative to{' '}
|
||||
<AnimatedText
|
||||
texts={[
|
||||
{
|
||||
text: 'Mixpanel',
|
||||
color: '#5028C0',
|
||||
},
|
||||
{
|
||||
text: 'Google Analytics',
|
||||
color: '#FAAE17',
|
||||
},
|
||||
{
|
||||
text: 'Plausible',
|
||||
color: '#5850EC',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Heading1>
|
||||
<Lead2 className="mb-12">
|
||||
The power of Mixpanel, the ease of Plausible and nothing from Google
|
||||
Analytics 😉
|
||||
</Lead2>
|
||||
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<ALink
|
||||
className="font-semibold"
|
||||
size="lg"
|
||||
href="https://dashboard.openpanel.dev/register"
|
||||
>
|
||||
Get started
|
||||
</ALink>
|
||||
</div>
|
||||
<div className="mt-8 flex justify-center gap-8 md:justify-start">
|
||||
<div>
|
||||
<div className=" uppercase text-muted-foreground">
|
||||
Collected events
|
||||
</div>
|
||||
<div className="font-serif text-3xl font-semibold">
|
||||
{shortNumber(eventCount)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className=" uppercase text-muted-foreground">
|
||||
Active projects
|
||||
</div>
|
||||
<div className="font-serif text-3xl font-semibold">
|
||||
{projectCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-12 h-[700px] w-full md:mt-36">
|
||||
<div className="absolute inset-0 flex rounded-2xl ring-8 ring-slate-300">
|
||||
<div className="absolute inset-0 w-full animate-pulse overflow-hidden rounded-2xl bg-slate-100" />
|
||||
<iframe
|
||||
src="https://dashboard.openpanel.dev/share/overview/zef2XC?header=0&range=30d"
|
||||
className="relative z-10 h-[700px] w-full rounded-2xl"
|
||||
title="Openpanel Dashboard"
|
||||
scrolling="no"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface JoinWaitlistProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function JoinWaitlistHero({ className }: JoinWaitlistProps) {
|
||||
const [value, setValue] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// @ts-ignore
|
||||
window.op('event', 'waitlist_success');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thanks so much!</DialogTitle>
|
||||
<DialogDescription>
|
||||
You're now on the waiting list. We'll let you know when
|
||||
we're ready. Should be within a month or two 🚀
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setOpen(false)}>Got it!</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<form
|
||||
className="w-full max-w-md"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
fetch('/api/waitlist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email: value }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((res) => {
|
||||
if (res.ok) {
|
||||
setOpen(true);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
placeholder="Enter your email"
|
||||
className={cn(
|
||||
'text-blue-darker h-12 w-full rounded-md border border-slate-100 bg-white px-4 shadow-sm outline-none ring-black focus:ring-1',
|
||||
className,
|
||||
)}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" className="absolute right-1 top-1">
|
||||
Join waitlist
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface JoinWaitlistProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function JoinWaitlist({ className }: JoinWaitlistProps) {
|
||||
const [value, setValue] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// @ts-ignore
|
||||
window.op('event', 'waitlist_success');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thanks so much!</DialogTitle>
|
||||
<DialogDescription>
|
||||
You're now on the waiting list. We'll let you know when
|
||||
we're ready. Should be within a month or two 🚀
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setOpen(false)}>Got it!</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<form
|
||||
className="w-full max-w-md"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
fetch('/api/waitlist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email: value }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((res) => {
|
||||
if (res.ok) {
|
||||
setOpen(true);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
placeholder="Enter your email"
|
||||
className={cn(
|
||||
'text-blue-darker h-12 w-full rounded-md border border-slate-100 bg-white px-4 shadow-sm outline-none ring-black focus:ring-1',
|
||||
className,
|
||||
)}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" className="absolute right-1 top-1">
|
||||
Join waitlist
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { Metadata } from 'next';
|
||||
import { Bricolage_Grotesque, Inter } from 'next/font/google';
|
||||
|
||||
import { OpenPanelComponent } from '@openpanel/nextjs';
|
||||
|
||||
import Footer from './footer';
|
||||
import { defaultMeta } from './meta';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
|
||||
import { Navbar } from './navbar';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
...defaultMeta,
|
||||
alternates: {
|
||||
canonical: 'https://openpanel.dev',
|
||||
},
|
||||
};
|
||||
|
||||
const head = Bricolage_Grotesque({
|
||||
display: 'swap',
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '700'],
|
||||
variable: '--font-serif',
|
||||
});
|
||||
const body = Inter({
|
||||
display: 'swap',
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '700'],
|
||||
variable: '--font-sans',
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="light bg-white">
|
||||
<body
|
||||
className={cn(
|
||||
'grainy min-h-screen font-sans text-slate-900 antialiased',
|
||||
head.variable,
|
||||
body.variable,
|
||||
)}
|
||||
>
|
||||
<Navbar darkText />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
|
||||
<OpenPanelComponent
|
||||
clientId="301c6dc1-424c-4bc3-9886-a8beab09b615"
|
||||
trackAttributes
|
||||
trackScreenViews
|
||||
trackOutgoingLinks
|
||||
/>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
import { defaultMeta } from './meta';
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: defaultMeta.title as string,
|
||||
short_name: 'Openpanel.dev',
|
||||
description: defaultMeta.description!,
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#fff',
|
||||
theme_color: '#fff',
|
||||
icons: [
|
||||
{
|
||||
src: '/favicon.ico',
|
||||
sizes: 'any',
|
||||
type: 'image/x-icon',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
const title = 'An open-source alternative to Mixpanel | Openpanel.dev';
|
||||
const description =
|
||||
'Unlock actionable insights effortlessly with Insightful, the open-source analytics library that combines the power of Mixpanel with the simplicity of Plausible. Enjoy a unified overview, predictable pricing, and a vibrant community. Join us in democratizing analytics today!';
|
||||
|
||||
export const defaultMeta: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
url: 'https://openpanel.dev',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://openpanel.dev/ogimage.png',
|
||||
width: 2011,
|
||||
height: 1339,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Logo } from '@/components/Logo';
|
||||
import { cn } from '@/utils/cn';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
interface Props {
|
||||
darkText?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Navbar({ darkText = false, className }: Props) {
|
||||
const pathname = usePathname();
|
||||
const textColor = darkText ? 'text-blue-dark' : 'text-white';
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed left-0 right-0 top-0 z-10 z-50 flex h-20 items-center border-b border-border bg-white',
|
||||
textColor,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="container flex items-center justify-between py-4">
|
||||
<Logo className="max-sm:[&_span]:hidden" />
|
||||
<nav className="flex gap-4 ">
|
||||
{pathname !== '/' && <Link href="/">Home</Link>}
|
||||
<Link href="/#pricing" data-event="click_pricing">
|
||||
Pricing
|
||||
</Link>
|
||||
<a href="https://docs.openpanel.dev" target="_blank" rel="noreferrer">
|
||||
Docs
|
||||
</a>
|
||||
<a href="https://git.new/openpanel" target="_blank" rel="noreferrer">
|
||||
Github
|
||||
</a>
|
||||
<a
|
||||
href="https://dashboard.openpanel.dev"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { ALink } from '@/components/ui/button';
|
||||
import { ExternalLinkIcon } from 'lucide-react';
|
||||
|
||||
import { Heading2, Lead2, Paragraph } from './copy';
|
||||
import { Features } from './features';
|
||||
import { Hero } from './hero';
|
||||
import { Pricing } from './pricing';
|
||||
import { PunchLines } from './punch-lines';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<Hero />
|
||||
<div className="bg-gradient-to-bl from-blue-600 to-blue-800 py-24">
|
||||
<div className="container">
|
||||
<Heading2 className="mb-2 leading-none text-white md:text-5xl">
|
||||
Analytics should be easy
|
||||
<br />
|
||||
and powerful
|
||||
</Heading2>
|
||||
<Lead2 className="text-white/80">
|
||||
The power of Mixpanel, the ease of Plausible and nothing from Google
|
||||
Analytics 😉 Curious how it looks?
|
||||
</Lead2>
|
||||
<ALink
|
||||
href="https://dashboard.openpanel.dev/share/overview/zef2XC"
|
||||
target="_blank"
|
||||
className="mt-8"
|
||||
variant={'outline'}
|
||||
>
|
||||
Check out the demo
|
||||
<ExternalLinkIcon className="ml-2 h-4 w-4" />
|
||||
</ALink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Features />
|
||||
|
||||
<PunchLines />
|
||||
|
||||
<Pricing />
|
||||
|
||||
<div className="container mt-40">
|
||||
<div className="flex flex-col gap-8 md:flex-row">
|
||||
<div className="relative mb-4 flex-shrink-0 md:w-1/2">
|
||||
<Heading2>Another analytic tool? Really?</Heading2>
|
||||
{/* <SirenIcon
|
||||
strokeWidth={0.5}
|
||||
size={300}
|
||||
className="opacity-10 absolute -rotate-12 -left-20 -top-10"
|
||||
/> */}
|
||||
</div>
|
||||
<div className="flex max-w-3xl flex-col gap-4">
|
||||
<h3 className="text-blue-dark text-lg font-bold">TL;DR</h3>
|
||||
<Paragraph>
|
||||
Our open-source analytic library fills a crucial gap by combining
|
||||
the strengths of Mixpanel's powerful features with
|
||||
Plausible's clear overview page. Motivated by the lack of an
|
||||
open-source alternative to Mixpanel and inspired by
|
||||
Plausible's simplicity, we aim to create an intuitive
|
||||
platform with predictable pricing. With a single-tier pricing
|
||||
model and limits only on monthly event counts, our goal is to
|
||||
democratize analytics, offering unrestricted access to all
|
||||
features while ensuring affordability and transparency for users
|
||||
of all project sizes.
|
||||
</Paragraph>
|
||||
|
||||
<h3 className="text-blue-dark mt-12 text-lg font-bold">The why</h3>
|
||||
<Paragraph>
|
||||
Our open-source analytic library emerged from a clear need within
|
||||
the analytics community. While platforms like Mixpanel offer
|
||||
powerful and user-friendly features, they lack a comprehensive
|
||||
overview page akin to Plausible's, which succinctly
|
||||
summarizes essential metrics. Recognizing this gap, we saw an
|
||||
opportunity to combine the strengths of both platforms while
|
||||
addressing their respective shortcomings.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
One significant motivation behind our endeavor was the absence of
|
||||
an open-source alternative to Mixpanel. We believe in the
|
||||
importance of accessibility and transparency in analytics, which
|
||||
led us to embark on creating a solution that anyone can freely use
|
||||
and contribute to.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Inspired by Plausible's exemplary approach to simplicity and
|
||||
clarity, we aim to build upon their foundation and further refine
|
||||
the user experience. By harnessing the best practices demonstrated
|
||||
by Plausible, we aspire to create an intuitive and streamlined
|
||||
analytics platform that empowers users to derive actionable
|
||||
insights effortlessly.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Our own experiences with traditional analytics platforms like
|
||||
Mixpanel underscored another critical aspect driving our project:
|
||||
the need for predictable pricing. As project owners ourselves, we
|
||||
encountered the frustration of escalating costs as our user base
|
||||
grew. Therefore, we are committed to offering a single-tier
|
||||
pricing model that provides unlimited access to all features
|
||||
without the fear of unexpected expenses.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
In line with our commitment to fairness and accessibility, our
|
||||
pricing model will only impose limits on the number of events
|
||||
users can send each month. This approach, akin to
|
||||
Plausible's, ensures that users have the freedom to explore
|
||||
and utilize our platform to its fullest potential without
|
||||
arbitrary restrictions on reports or user counts. Ultimately, our
|
||||
goal is to democratize analytics by offering a reliable,
|
||||
transparent, and cost-effective solution for projects of all
|
||||
sizes.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { CheckIcon, StarIcon } from 'lucide-react';
|
||||
|
||||
import { Heading2, Lead2 } from './copy';
|
||||
|
||||
const pricing = [
|
||||
{ price: 'Free', events: 5000, hint: 'Try it' },
|
||||
{ price: '$5', events: 10_000 },
|
||||
{ price: '$10', events: 100_000 },
|
||||
{ price: '$20', events: 200_000, hint: 'Great value' },
|
||||
{ price: '$40', events: 500_000 },
|
||||
{ price: '$60', events: 1_000_000 },
|
||||
{ price: '$80', events: 2_000_000 },
|
||||
{ price: '$120', events: 5_000_000 },
|
||||
{ price: '$150', events: 10_000_000 },
|
||||
];
|
||||
|
||||
export function Pricing() {
|
||||
return (
|
||||
<div className="bg-slate-100 py-32" id="pricing">
|
||||
<div className="mx-auto px-4 sm:max-w-xl md:max-w-3xl">
|
||||
<section className="flex flex-col gap-6">
|
||||
<div className="flex w-full flex-col gap-4 md:max-w-[58rem]">
|
||||
<Heading2>Simple, transparent pricing</Heading2>
|
||||
<Lead2 className="max-w-[85%] leading-normal text-muted-foreground sm:text-lg sm:leading-7">
|
||||
Everything is included, just decide how many events you want to
|
||||
track each month.
|
||||
</Lead2>
|
||||
</div>
|
||||
<ul className="grid gap-3 text-muted-foreground sm:grid-cols-2 md:grid-cols-3">
|
||||
<li className="flex items-center">
|
||||
<CheckIcon className="mr-2 h-4 w-4" /> Unlimited websites/apps
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<CheckIcon className="mr-2 h-4 w-4" /> Unlimited users
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<CheckIcon className="mr-2 h-4 w-4" /> Unlimted dashboards
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<CheckIcon className="mr-2 h-4 w-4" /> Unlimted charts
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<CheckIcon className="mr-2 h-4 w-4" /> Unlimted tracked profiles
|
||||
</li>
|
||||
<li className="flex items-center font-bold text-slate-900">
|
||||
<CheckIcon className="mr-2 h-4 w-4" /> Yes, its that simple
|
||||
</li>
|
||||
</ul>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{pricing.map((item) => (
|
||||
<div
|
||||
key={item.price}
|
||||
className="border-blue-dark relative flex flex-col gap-1 rounded-lg border bg-white p-6"
|
||||
>
|
||||
{item.hint && (
|
||||
<div className="absolute right-2 top-2 flex items-center gap-2 rounded bg-blue-600 px-2 py-1 text-sm text-white">
|
||||
<StarIcon size={12} />
|
||||
{item.hint}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-3xl font-bold">{item.price}</div>
|
||||
<div className="flex justify-between text-lg">
|
||||
<span>
|
||||
{new Intl.NumberFormat('en').format(item.events)} events
|
||||
</span>
|
||||
<span className="text-muted-foreground">per month</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="font-bold">Everything is free during beta period!</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
import {
|
||||
ClockIcon,
|
||||
CloudIcon,
|
||||
CookieIcon,
|
||||
DollarSignIcon,
|
||||
HandshakeIcon,
|
||||
KeyIcon,
|
||||
ShieldIcon,
|
||||
WebhookIcon,
|
||||
} from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Heading2, Heading4 } from './copy';
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: 'Own Your Own Data',
|
||||
description: (
|
||||
<p>
|
||||
We believe that you should own your own data. That's why we
|
||||
don't sell your data to third parties.{' '}
|
||||
<strong>Ever. Period.</strong>
|
||||
</p>
|
||||
),
|
||||
icon: KeyIcon,
|
||||
color: '#2563EB',
|
||||
className: 'bg-blue-light',
|
||||
},
|
||||
{
|
||||
title: 'GDPR Compliant',
|
||||
description: (
|
||||
<p>
|
||||
All our serveres are hosted in EU (Stockholm) and we are fully GDPR
|
||||
compliant.
|
||||
</p>
|
||||
),
|
||||
icon: ShieldIcon,
|
||||
color: '#b051d3',
|
||||
className: 'bg-[#b051d3]',
|
||||
},
|
||||
{
|
||||
title: 'Cloud or Self-Hosting',
|
||||
description: (
|
||||
<p>
|
||||
Choose between the flexibility of cloud-based hosting or the autonomy of
|
||||
self-hosting to tailor your analytics infrastructure to your needs.
|
||||
</p>
|
||||
),
|
||||
icon: CloudIcon,
|
||||
color: '#ff7557',
|
||||
className: '', // 'bg-[#ff7557]',
|
||||
},
|
||||
{
|
||||
title: 'Real-Time Events',
|
||||
description: (
|
||||
<p>
|
||||
Stay up-to-date with real-time event tracking, enabling instant insights
|
||||
into user actions as they happen.
|
||||
</p>
|
||||
),
|
||||
icon: ClockIcon,
|
||||
color: '#7fe1d8',
|
||||
className: '', // bg-[#7fe1d8]
|
||||
},
|
||||
{
|
||||
title: 'No cookies!',
|
||||
description: (
|
||||
<p>
|
||||
Our trackers are cookie-free, skip that annyoing cookie consent banner!
|
||||
</p>
|
||||
),
|
||||
icon: CookieIcon,
|
||||
color: '#f8bc3c',
|
||||
className: 'bg-blue-dark', //'bg-[#f8bc3c]',
|
||||
},
|
||||
{
|
||||
title: 'Cost-Effective',
|
||||
description: (
|
||||
<p>
|
||||
We have combined the best from Mixpanel and Plausible. Cut the costs and
|
||||
keep the features.
|
||||
</p>
|
||||
),
|
||||
icon: DollarSignIcon,
|
||||
color: '#0f7ea0',
|
||||
className: 'bg-[#3ba974]',
|
||||
},
|
||||
{
|
||||
title: 'Predictable pricing',
|
||||
description: (
|
||||
<p>You only pay for events, everything else is included. No surprises.</p>
|
||||
),
|
||||
icon: HandshakeIcon,
|
||||
color: '#0f7ea0',
|
||||
className: 'bg-[#3ba974]',
|
||||
},
|
||||
{
|
||||
title: 'First Class React Native Support',
|
||||
description: (
|
||||
<p>
|
||||
Our SDK is built with React Native in mind, making it easy to integrate
|
||||
with your mobile apps.
|
||||
</p>
|
||||
),
|
||||
icon: (({ className }: LucideProps) => {
|
||||
return (
|
||||
<Image
|
||||
src="/react-native.svg"
|
||||
alt="React Native"
|
||||
className={cn(className, 'p-3')}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
);
|
||||
}) as unknown as LucideIcon,
|
||||
color: '#3ba974',
|
||||
className: 'bg-[#e19900]',
|
||||
},
|
||||
{
|
||||
title: 'Powerful Export API',
|
||||
description: <p>Use our powerful export API to access your data.</p>,
|
||||
icon: WebhookIcon,
|
||||
color: '#3ba974',
|
||||
className: 'bg-[#e93838]',
|
||||
},
|
||||
];
|
||||
|
||||
export function PunchLines() {
|
||||
return (
|
||||
<div className="bg-blue-darker relative py-32">
|
||||
<div className="absolute inset-0 h-full w-full bg-[radial-gradient(circle,rgba(255,255,255,0.2)_0%,rgba(255,255,255,0)_100%)]" />
|
||||
<div className="relative">
|
||||
<Heading2 className="mb-16 text-center text-white">
|
||||
Not convinced?
|
||||
</Heading2>
|
||||
<div className="container">
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl border border-border bg-white p-6"
|
||||
key={item.title}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mb-4 flex h-14 w-14 items-center justify-center rounded-full',
|
||||
item.color,
|
||||
)}
|
||||
style={{ background: item.color }}
|
||||
>
|
||||
<Icon color="#fff" />
|
||||
</div>
|
||||
<Heading4>{item.title}</Heading4>
|
||||
<div className="prose">{item.description}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
|
||||
import { db } from '@openpanel/db';
|
||||
|
||||
import { SocialProof } from './social-proof';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
export async function SocialProofServer(props: Props) {
|
||||
const waitlistCount = await db.waitlist.count();
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<SocialProof count={waitlistCount} {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { cn } from '@/utils/cn';
|
||||
// import { StarIcon } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface JoinWaitlistProps {
|
||||
className?: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function SocialProof({ className, count }: JoinWaitlistProps) {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', className)}>
|
||||
<div className="flex">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src="/clickhouse.png"
|
||||
width={40}
|
||||
height={40}
|
||||
alt="Clickhouse"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Clickhouse is here</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="-mx-3">
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src="/getdreams.png"
|
||||
width={40}
|
||||
height={40}
|
||||
alt="GetDreams"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>GetDreams is here</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src="/kiddokitchen.png"
|
||||
width={40}
|
||||
height={40}
|
||||
alt="KiddoKitchen"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>KiddoKitchen is here</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-left">{count} early birds have signed up! 🚀</p>
|
||||
{/* <div className="flex gap-0.5">
|
||||
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
|
||||
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
|
||||
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
|
||||
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
|
||||
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// <div class="flex flex-col gap-y-2 mt-5 lg:mt-3"><p class="text-gray-700 dark:text-gray-100 text-sm w-24 text-start font-semibold whitespace-nowrap">What users think</p><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"/><img class="w-5 h-5 rounded-full " src="https://pbs.twimg.com/profile_images/1744063824431370240/BbVtyCiy_normal.png" alt="feedback_0"><span class="flex-wrap text-sm text-gray-600 dark:text-gray-200 text-start font-normal">“ Been a long time Mixpanel user and without a doubt there's a bunch of room to innovate. I'm confident Openpanel is on the right path! ”</span/><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame w-5 h-5 text-red-600 fill-orange-400 animate-pulse"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg/><img class="w-5 h-5 rounded-full border border-2 border-red-500" src="https://pbs.twimg.com/profile_images/1751607056316944384/8E4F88FL_normal.jpg" alt="feedback_1"><span class="flex-wrap text-sm text-gray-600 dark:text-gray-200 text-start font-normal">“ I have used Openpanel for the last 6 months (since I’m the creator) for 3 different sites/apps. It’s a great analytics product that has everything you need. Still lacking a native app but will work hard to make that a reality! ”</span/><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"/><img class="w-5 h-5 rounded-full " src="https://pbs.twimg.com/profile_images/1701887174042324992/g2GBIQay_normal.jpg" alt="feedback_2"><span class="flex-wrap text-sm text-gray-600 dark:text-gray-200 text-start font-normal">“ would be cool if it was easier to edit text after image is generated ”</span/><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame w-5 h-5 text-red-600 fill-orange-400 animate-pulse"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg/><img class="w-5 h-5 rounded-full border border-2 border-red-500" src="https://pbs.twimg.com/profile_images/1194368464946974728/1D2biimN_normal.jpg" alt="feedback_3"><span class="flex-wrap text-sm text-gray-600 dark:text-gray-200 text-start font-normal">“ Awesome product, very easy to use and understand. I miss a native app and the documentation could be improved. Otherwise I love it. ”</span/><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"/><img class="w-5 h-5 rounded-full " src="https://lh3.googleusercontent.com/a/ACg8ocIWiGTd3nWE5etp-CFhxrTKFvSLSJJd7pPmiM9SNJ9sAg=s96-c" alt="feedback_4"><span class="flex-wrap text-sm text-gray-600 dark:text-gray-200 text-start font-normal">“ I have used Open panel since the private beta and i'm super impressed by the product already, the speed after you give feedback to actually get the features is truly amazing! Can't wait to see where Openpanel are in 6 months! ”</span/><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"/><img class="w-5 h-5 rounded-full " src="https://lh3.googleusercontent.com/a/ACg8ocKymAw_YoIrfoGp-bWMlDsXgM6St0dzaVJ7m_lGNXDtrA=s96-c" alt="feedback_5"><span class="flex-wrap text-sm text-gray-600 dark:text-gray-200 text-start font-normal">“ Impressively fast UI and easy to integrate! Added it alongside my current analytics tool for my native app in less than an hour. ”</span/><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"/><img class="w-5 h-5 rounded-full " src="https://pbs.twimg.com/profile_images/1735771119980879872/Mx5MlB9e_normal.jpg" alt="feedback_6"><span class="flex-wrap text-sm text-gray-600 dark:text-gray-200 text-start font-normal">“ Im using plausible and find it pleasing but limited.
|
||||
// Im looking forward to trying out Openpanel, the demo pictures and the page look professional. The listed features seem to be broader then plausible. :) ”</span/><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"/><img class="w-5 h-5 rounded-full " src="https://pbs.twimg.com/profile_images/1767459527006334976/unbMENPG_normal.jpg" alt="feedback_7"><span class="flex-wrap text-sm text-gray-600 dark:text-gray-200 text-start font-normal">“ Incredibly easy to implement and a joy to use. 5/5 would recommend. ”</span/></div>
|
||||
@@ -1,25 +0,0 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface LogoProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Logo({ className }: LogoProps) {
|
||||
return (
|
||||
<Link
|
||||
href="/"
|
||||
className={cn('flex items-center gap-2 text-xl font-medium', className)}
|
||||
>
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
className="max-h-8 rounded-md"
|
||||
alt="Openpanel logo"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<span>openpanel.dev</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex flex-shrink-0 items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
cta: 'bg-blue-600 text-primary-foreground hover:bg-blue-500',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
loading?: boolean;
|
||||
icon?: LucideIcon;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
children,
|
||||
loading,
|
||||
disabled,
|
||||
icon,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
const Icon = loading ? Loader2 : (icon ?? null);
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
disabled={loading || disabled}
|
||||
{...props}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon className={cn('mr-2 h-4 w-4', loading && 'animate-spin')} />
|
||||
)}
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
Button.defaultProps = {
|
||||
type: 'button',
|
||||
};
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
||||
export interface ALinkProps
|
||||
extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
export const ALink = ({
|
||||
variant,
|
||||
size,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ALinkProps) => {
|
||||
return (
|
||||
<a {...props} className={cn(buttonVariants({ variant, size, className }))}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -1,257 +0,0 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import useEmblaCarousel from 'embla-carousel-react';
|
||||
import type { UseEmblaCarouselType } from 'embla-carousel-react';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
interface CarouselProps {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a <Carousel />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(api);
|
||||
api.on('reInit', onSelect);
|
||||
api.on('select', onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<section
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative', className)}
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
Carousel.displayName = 'Carousel';
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CarouselContent.displayName = 'CarouselContent';
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CarouselItem.displayName = 'CarouselItem';
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? '-left-12 top-1/2 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselPrevious.displayName = 'CarouselPrevious';
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? '-right-12 top-1/2 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = 'CarouselNext';
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-100%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full md:translate-y-[-50%]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = 'DialogHeader';
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = 'DialogFooter';
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn(' text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as React from 'react';
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
error?: string | undefined;
|
||||
};
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, error, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'file: flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 ring-offset-background file:border-0 file:bg-transparent file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
!!error && 'border-destructive',
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
@@ -1,30 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import * as React from 'react';
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import * as React from 'react';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-black px-3 py-1.5 text-popover shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -1,49 +0,0 @@
|
||||
import { createEnv } from '@t3-oss/env-nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const env = createEnv({
|
||||
/**
|
||||
* Specify your server-side environment variables schema here. This way you can ensure the app
|
||||
* isn't built with invalid env vars.
|
||||
*/
|
||||
server: {
|
||||
DATABASE_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.refine(
|
||||
(str) => !str.includes('DATABASE_URL'),
|
||||
'You forgot to change the default URL',
|
||||
),
|
||||
NODE_ENV: z
|
||||
.enum(['development', 'test', 'production'])
|
||||
.default('development'),
|
||||
},
|
||||
|
||||
/**
|
||||
* Specify your client-side environment variables schema here. This way you can ensure the app
|
||||
* isn't built with invalid env vars. To expose them to the client, prefix them with
|
||||
* `NEXT_PUBLIC_`.
|
||||
*/
|
||||
client: {
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
},
|
||||
|
||||
/**
|
||||
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
|
||||
* middlewares) or client-side so we need to destruct manually.
|
||||
*/
|
||||
runtimeEnv: {
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
},
|
||||
/**
|
||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||
* useful for Docker builds.
|
||||
*/
|
||||
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||
/**
|
||||
* Makes it so that empty strings are treated as undefined.
|
||||
* `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error.
|
||||
*/
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
}
|
||||
|
||||
.fancy-text {
|
||||
@apply inline-block bg-gradient-to-br from-blue-200 to-blue-400 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.dashed {
|
||||
--color: #fff;
|
||||
background-image: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--color) 0%,
|
||||
var(--color) 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
to right,
|
||||
var(--color) 0%,
|
||||
var(--color) 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--color) 0%,
|
||||
var(--color) 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--color) 0%,
|
||||
var(--color) 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-position: left top, left bottom, left top, right top;
|
||||
background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
|
||||
background-size: 15px 2px, 15px 2px, 2px 15px, 2px 15px;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { clsx } from 'clsx';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user