+ {guide.title} +
+ {guide.description && ( +{guide.description}
+ )} ++ {formatDate(guide.date)} +
+ )} ++ Start at full speed with Dingify ! +
+ + ++ Introduction ยท + Installation ยท + Tech Stack + Features ยท + Author ยท + Credits +
++ Enter your email to sign in to your account +
++ + Don't have an account? Sign Up + +
++ Enter your email below to create your account +
++ By clicking continue, you agree to our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + . +
+{guide.description}
+ )} ++ {formatDate(guide.date)} +
+ )} +No guides published.
+ )} +{page.description}
+ )} +{author.title}
++ @{author.twitter} +
+
+
+ Lets explore{" "} + Dingify{" "} +
+ +New users today
+New users this week
+New users this month
+Total users
++ {subscriptionPlan.isCanceled + ? "Your plan will be canceled on " + : "Your plan renews on "} + {formatDate(subscriptionPlan.stripeCurrentPeriodEnd)}. +
+ ) : null} +
+
+ {post.description} +
+ )} + {post.date && ( ++ {formatDate(post.date)} +
+ )} + + View Article + +*]:text-muted-foreground", + className + )} + {...props} + /> + ), + img: ({ + className, + alt, + ...props + }: React.ImgHTMLAttributes) => ( + // eslint-disable-next-line @next/next/no-img-element + + ), + hr: ({ ...props }) =>
, + table: ({ className, ...props }: React.HTMLAttributes) => ( + ++ ), + tr: ({ className, ...props }: React.HTMLAttributes+
) => ( + + ), + th: ({ className, ...props }) => ( + + ), + td: ({ className, ...props }) => ( + + ), + pre: ({ className, ...props }) => ( + + ), + code: ({ className, ...props }) => ( + + ), + Image: (props: ImageProps) =>
, + Callout, + Card: MdxCard, +}; + +interface MdxProps { + code: string; +} + +export function Mdx({ code }: MdxProps) { + const Component = useMDXComponent(code); + + return ( + + {/* @ts-expect-error */} ++ ); +} diff --git a/apps/www/src/components/content/templete-blog/deploying-next-apps.mdx b/apps/www/src/components/content/templete-blog/deploying-next-apps.mdx new file mode 100644 index 0000000..2599430 --- /dev/null +++ b/apps/www/src/components/content/templete-blog/deploying-next-apps.mdx @@ -0,0 +1,219 @@ +--- +title: Deploying Next.js Apps +description: How to deploy your Next.js apps on Vercel. +image: /images/blog/blog-post-3.jpg +date: "2023-01-02" +authors: + - shadcn +--- + ++ + The text below is from the [Tailwind + CSS](https://play.tailwindcss.com/uj1vGACRJA?layout=preview) docs. I copied it + here to test the markdown styles. **Tailwind is awesome. You should use it.** + + +Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS. + +By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. + +We get lots of complaints about it actually, with people regularly asking us things like: + +> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? +> We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either โ you want them to look _awesome_, not awful. + +The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. + +It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: + +```html ++ +``` + +For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). + +--- + +## What to expect from here on out + +What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. + +It's important to cover all of these use cases for a few reasons: + +1. We want everything to look good out of the box. +2. Really just the first reason, that's the whole point of the plugin. +3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. + +Now we're going to try out another header style. + +### Typography should be easy + +So that's a header for you โ with any luck if we've done our job correctly that will look pretty reasonable. + +Something a wise person once told me about typography is: + +> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. + +It's probably important that images look okay here by default as well: + +Garlic bread with cheese: What the science tells us
++ For years parents have espoused the health benefits of eating garlic bread + with cheese to their children, with the food earning such an iconic status + in our culture that kids will often dress up as warm, cheesy loaf for + Halloween. +
++ But a recent study shows that the celebrated appetizer may be linked to a + series of rabies cases springing up around the country. +
++ +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. + +Now I'm going to show you an example of an unordered list to make sure that looks good, too: + +- So here is the first item in this list. +- In this example we're keeping the items short. +- Later, we'll use longer, more complex list items. + +And that's the end of this section. + +## What if we stack headings? + +### We should make sure that looks good, too. + +Sometimes you have headings directly underneath each other. In those cases you often have to undo the top margin on the second heading because it usually looks better for the headings to be closer together than a paragraph followed by a heading should be. + +### When a heading comes after a paragraph โฆ + +When a heading comes after a paragraph, we need a bit more space, like I already mentioned above. Now let's see what a more complex list would look like. + +- **I often do this thing where list items have headings.** + + For some reason I think this looks cool which is unfortunate because it's pretty annoying to get the styles right. + + I often have two or three paragraphs in these list items, too, so the hard part is getting the spacing between the paragraphs, list item heading, and separate list items to all make sense. Pretty tough honestly, you could make a strong argument that you just shouldn't write this way. + +- **Since this is a list, I need at least two items.** + + I explained what I'm doing already in the previous list item, but a list wouldn't be a list if it only had one item, and we really want this to look realistic. That's why I've added this second list item so I actually have something to look at when writing the styles. + +- **It's not a bad idea to add a third item either.** + + I think it probably would've been fine to just use two items but three is definitely not worse, and since I seem to be having no trouble making up arbitrary things to type, I might as well include it. + +After this sort of list I usually have a closing statement or paragraph, because it kinda looks weird jumping right to a heading. + +## Code should look okay by default. + +I think most people are going to use [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/) or something if they want to style their code blocks but it wouldn't hurt to make them look _okay_ out of the box, even with no syntax highlighting. + +Here's what a default `tailwind.config.js` file looks like at the time of writing: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +Hopefully that looks good enough to you. + +### What about nested lists? + +Nested lists basically always look bad which is why editors like Medium don't even let you do it, but I guess since some of you goofballs are going to do it we have to carry the burden of at least making it work. + +1. **Nested lists are rarely a good idea.** + - You might feel like you are being really "organized" or something but you are just creating a gross shape on the screen that is hard to read. + - Nested navigation in UIs is a bad idea too, keep things as flat as possible. + - Nesting tons of folders in your source code is also not helpful. +2. **Since we need to have more items, here's another one.** + - I'm not sure if we'll bother styling more than two levels deep. + - Two is already too much, three is guaranteed to be a bad idea. + - If you nest four levels deep you belong in prison. +3. **Two items isn't really a list, three is good though.** + - Again please don't nest lists if you want people to actually read your content. + - Nobody wants to look at this. + - I'm upset that we even have to bother styling this. + +The most annoying thing about lists in Markdown is that ` ` elements aren't given a child ` ` tag unless there are multiple paragraphs in the list item. That means I have to worry about styling that annoying situation too. + +- **For example, here's another nested list.** + + But this time with a second paragraph. + + - These list items won't have `
` tags + - Because they are only one line each + +- **But in this second top-level list item, they will.** + + This is especially annoying because of the spacing on this paragraph. + + - As you can see here, because I've added a second line, this list item now has a `
` tag. + + This is the second line I'm talking about by the way. + + - Finally here's another list item so it's more like a list. + +- A closing list item, but with no nested list, because why not? + +And finally a sentence to close off this section. + +## There are other elements we need to style + +I almost forgot to mention links, like [this link to the Tailwind CSS website](https://tailwindcss.com). We almost made them blue but that's so yesterday, so we went with dark gray, feels edgier. + +We even included table styles, check it out: + +| Wrestler | Origin | Finisher | +| ----------------------- | ------------ | ------------------ | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +We also need to make sure inline code looks good, like if I wanted to talk about `` elements or tell you the good news about `@tailwindcss/typography`. + +### Sometimes I even use `code` in headings + +Even though it's probably a bad idea, and historically I've had a hard time making it look good. This _"wrap the code blocks in backticks"_ trick works pretty well though really. + +Another thing I've done in the past is put a `code` tag inside of a link, like if I wanted to tell you about the [`tailwindcss/docs`](https://github.com/tailwindcss/docs) repository. I don't love that there is an underline below the backticks but it is absolutely not worth the madness it would require to avoid it. + +#### We haven't used an `h4` yet + +But now we have. Please don't use `h5` or `h6` in your content, Medium only supports two heading levels for a reason, you animals. I honestly considered using a `before` pseudo-element to scream at you if you use an `h5` or `h6`. + +We don't style them at all out of the box because `h4` elements are already so small that they are the same size as the body copy. What are we supposed to do with an `h5`, make it _smaller_ than the body copy? No thanks. + +### We still need to think about stacked headings though. + +#### Let's make sure we don't screw that up with `h4` elements, either. + +Phew, with any luck we have styled the headings above this text and they look pretty good. + +Let's add a closing paragraph here so things end with a decently sized block of text. I can't explain why I want things to end that way but I have to assume it's because I think things will look weird or unbalanced if there is a heading too close to the end of the document. + +What I've written here is probably long enough, but adding this final sentence can't hurt. + +## GitHub Flavored Markdown + +I've also added support for GitHub Flavored Mardown using `remark-gfm`. + +With `remark-gfm`, we get a few extra features in our markdown. Example: autolink literals. + +A link like www.example.com or https://example.com would automatically be converted into an `a` tag. + +This works for email links too: contact@example.com. diff --git a/apps/www/src/components/content/templete-blog/dynamic-routing-static-regeneration copy.mdx b/apps/www/src/components/content/templete-blog/dynamic-routing-static-regeneration copy.mdx new file mode 100644 index 0000000..5a0ac70 --- /dev/null +++ b/apps/www/src/components/content/templete-blog/dynamic-routing-static-regeneration copy.mdx @@ -0,0 +1,219 @@ +--- +title: Dynamic Routing and Static Regeneration +description: How to use incremental static regeneration using dynamic routes. +image: /images/blog/blog-post-2.jpg +date: "2023-03-04" +authors: + - codehagen +--- + +
+ The text below is from the [Tailwind + CSS](https://play.tailwindcss.com/uj1vGACRJA?layout=preview) docs. I copied it + here to test the markdown styles. **Tailwind is awesome. You should use it.** + + +Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS. + +By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. + +We get lots of complaints about it actually, with people regularly asking us things like: + +> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? +> We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either โ you want them to look _awesome_, not awful. + +The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. + +It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: + +```html ++ +``` + +For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). + +--- + +## What to expect from here on out + +What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. + +It's important to cover all of these use cases for a few reasons: + +1. We want everything to look good out of the box. +2. Really just the first reason, that's the whole point of the plugin. +3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. + +Now we're going to try out another header style. + +### Typography should be easy + +So that's a header for you โ with any luck if we've done our job correctly that will look pretty reasonable. + +Something a wise person once told me about typography is: + +> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. + +It's probably important that images look okay here by default as well: + +Garlic bread with cheese: What the science tells us
++ For years parents have espoused the health benefits of eating garlic bread + with cheese to their children, with the food earning such an iconic status + in our culture that kids will often dress up as warm, cheesy loaf for + Halloween. +
++ But a recent study shows that the celebrated appetizer may be linked to a + series of rabies cases springing up around the country. +
++ +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. + +Now I'm going to show you an example of an unordered list to make sure that looks good, too: + +- So here is the first item in this list. +- In this example we're keeping the items short. +- Later, we'll use longer, more complex list items. + +And that's the end of this section. + +## What if we stack headings? + +### We should make sure that looks good, too. + +Sometimes you have headings directly underneath each other. In those cases you often have to undo the top margin on the second heading because it usually looks better for the headings to be closer together than a paragraph followed by a heading should be. + +### When a heading comes after a paragraph โฆ + +When a heading comes after a paragraph, we need a bit more space, like I already mentioned above. Now let's see what a more complex list would look like. + +- **I often do this thing where list items have headings.** + + For some reason I think this looks cool which is unfortunate because it's pretty annoying to get the styles right. + + I often have two or three paragraphs in these list items, too, so the hard part is getting the spacing between the paragraphs, list item heading, and separate list items to all make sense. Pretty tough honestly, you could make a strong argument that you just shouldn't write this way. + +- **Since this is a list, I need at least two items.** + + I explained what I'm doing already in the previous list item, but a list wouldn't be a list if it only had one item, and we really want this to look realistic. That's why I've added this second list item so I actually have something to look at when writing the styles. + +- **It's not a bad idea to add a third item either.** + + I think it probably would've been fine to just use two items but three is definitely not worse, and since I seem to be having no trouble making up arbitrary things to type, I might as well include it. + +After this sort of list I usually have a closing statement or paragraph, because it kinda looks weird jumping right to a heading. + +## Code should look okay by default. + +I think most people are going to use [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/) or something if they want to style their code blocks but it wouldn't hurt to make them look _okay_ out of the box, even with no syntax highlighting. + +Here's what a default `tailwind.config.js` file looks like at the time of writing: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +Hopefully that looks good enough to you. + +### What about nested lists? + +Nested lists basically always look bad which is why editors like Medium don't even let you do it, but I guess since some of you goofballs are going to do it we have to carry the burden of at least making it work. + +1. **Nested lists are rarely a good idea.** + - You might feel like you are being really "organized" or something but you are just creating a gross shape on the screen that is hard to read. + - Nested navigation in UIs is a bad idea too, keep things as flat as possible. + - Nesting tons of folders in your source code is also not helpful. +2. **Since we need to have more items, here's another one.** + - I'm not sure if we'll bother styling more than two levels deep. + - Two is already too much, three is guaranteed to be a bad idea. + - If you nest four levels deep you belong in prison. +3. **Two items isn't really a list, three is good though.** + - Again please don't nest lists if you want people to actually read your content. + - Nobody wants to look at this. + - I'm upset that we even have to bother styling this. + +The most annoying thing about lists in Markdown is that ` ` elements aren't given a child ` ` tag unless there are multiple paragraphs in the list item. That means I have to worry about styling that annoying situation too. + +- **For example, here's another nested list.** + + But this time with a second paragraph. + + - These list items won't have `
` tags + - Because they are only one line each + +- **But in this second top-level list item, they will.** + + This is especially annoying because of the spacing on this paragraph. + + - As you can see here, because I've added a second line, this list item now has a `
` tag. + + This is the second line I'm talking about by the way. + + - Finally here's another list item so it's more like a list. + +- A closing list item, but with no nested list, because why not? + +And finally a sentence to close off this section. + +## There are other elements we need to style + +I almost forgot to mention links, like [this link to the Tailwind CSS website](https://tailwindcss.com). We almost made them blue but that's so yesterday, so we went with dark gray, feels edgier. + +We even included table styles, check it out: + +| Wrestler | Origin | Finisher | +| ----------------------- | ------------ | ------------------ | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +We also need to make sure inline code looks good, like if I wanted to talk about `` elements or tell you the good news about `@tailwindcss/typography`. + +### Sometimes I even use `code` in headings + +Even though it's probably a bad idea, and historically I've had a hard time making it look good. This _"wrap the code blocks in backticks"_ trick works pretty well though really. + +Another thing I've done in the past is put a `code` tag inside of a link, like if I wanted to tell you about the [`tailwindcss/docs`](https://github.com/tailwindcss/docs) repository. I don't love that there is an underline below the backticks but it is absolutely not worth the madness it would require to avoid it. + +#### We haven't used an `h4` yet + +But now we have. Please don't use `h5` or `h6` in your content, Medium only supports two heading levels for a reason, you animals. I honestly considered using a `before` pseudo-element to scream at you if you use an `h5` or `h6`. + +We don't style them at all out of the box because `h4` elements are already so small that they are the same size as the body copy. What are we supposed to do with an `h5`, make it _smaller_ than the body copy? No thanks. + +### We still need to think about stacked headings though. + +#### Let's make sure we don't screw that up with `h4` elements, either. + +Phew, with any luck we have styled the headings above this text and they look pretty good. + +Let's add a closing paragraph here so things end with a decently sized block of text. I can't explain why I want things to end that way but I have to assume it's because I think things will look weird or unbalanced if there is a heading too close to the end of the document. + +What I've written here is probably long enough, but adding this final sentence can't hurt. + +## GitHub Flavored Markdown + +I've also added support for GitHub Flavored Mardown using `remark-gfm`. + +With `remark-gfm`, we get a few extra features in our markdown. Example: autolink literals. + +A link like www.example.com or https://example.com would automatically be converted into an `a` tag. + +This works for email links too: contact@example.com. diff --git a/apps/www/src/components/content/templete-blog/preview-mode-headless-cms.mdx b/apps/www/src/components/content/templete-blog/preview-mode-headless-cms.mdx new file mode 100644 index 0000000..e90e247 --- /dev/null +++ b/apps/www/src/components/content/templete-blog/preview-mode-headless-cms.mdx @@ -0,0 +1,219 @@ +--- +title: Preview Mode for Headless CMS +description: How to implement preview mode in your headless CMS. +date: "2023-04-09" +image: /images/blog/blog-post-1.jpg +authors: + - shadcn +--- + +
+ The text below is from the [Tailwind + CSS](https://play.tailwindcss.com/uj1vGACRJA?layout=preview) docs. I copied it + here to test the markdown styles. **Tailwind is awesome. You should use it.** + + +Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS. + +By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. + +We get lots of complaints about it actually, with people regularly asking us things like: + +> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? +> We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either โ you want them to look _awesome_, not awful. + +The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. + +It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: + +```html ++ +``` + +For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). + +--- + +## What to expect from here on out + +What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. + +It's important to cover all of these use cases for a few reasons: + +1. We want everything to look good out of the box. +2. Really just the first reason, that's the whole point of the plugin. +3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. + +Now we're going to try out another header style. + +### Typography should be easy + +So that's a header for you โ with any luck if we've done our job correctly that will look pretty reasonable. + +Something a wise person once told me about typography is: + +> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. + +It's probably important that images look okay here by default as well: + +Garlic bread with cheese: What the science tells us
++ For years parents have espoused the health benefits of eating garlic bread + with cheese to their children, with the food earning such an iconic status + in our culture that kids will often dress up as warm, cheesy loaf for + Halloween. +
++ But a recent study shows that the celebrated appetizer may be linked to a + series of rabies cases springing up around the country. +
++ +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. + +Now I'm going to show you an example of an unordered list to make sure that looks good, too: + +- So here is the first item in this list. +- In this example we're keeping the items short. +- Later, we'll use longer, more complex list items. + +And that's the end of this section. + +## What if we stack headings? + +### We should make sure that looks good, too. + +Sometimes you have headings directly underneath each other. In those cases you often have to undo the top margin on the second heading because it usually looks better for the headings to be closer together than a paragraph followed by a heading should be. + +### When a heading comes after a paragraph โฆ + +When a heading comes after a paragraph, we need a bit more space, like I already mentioned above. Now let's see what a more complex list would look like. + +- **I often do this thing where list items have headings.** + + For some reason I think this looks cool which is unfortunate because it's pretty annoying to get the styles right. + + I often have two or three paragraphs in these list items, too, so the hard part is getting the spacing between the paragraphs, list item heading, and separate list items to all make sense. Pretty tough honestly, you could make a strong argument that you just shouldn't write this way. + +- **Since this is a list, I need at least two items.** + + I explained what I'm doing already in the previous list item, but a list wouldn't be a list if it only had one item, and we really want this to look realistic. That's why I've added this second list item so I actually have something to look at when writing the styles. + +- **It's not a bad idea to add a third item either.** + + I think it probably would've been fine to just use two items but three is definitely not worse, and since I seem to be having no trouble making up arbitrary things to type, I might as well include it. + +After this sort of list I usually have a closing statement or paragraph, because it kinda looks weird jumping right to a heading. + +## Code should look okay by default. + +I think most people are going to use [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/) or something if they want to style their code blocks but it wouldn't hurt to make them look _okay_ out of the box, even with no syntax highlighting. + +Here's what a default `tailwind.config.js` file looks like at the time of writing: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +Hopefully that looks good enough to you. + +### What about nested lists? + +Nested lists basically always look bad which is why editors like Medium don't even let you do it, but I guess since some of you goofballs are going to do it we have to carry the burden of at least making it work. + +1. **Nested lists are rarely a good idea.** + - You might feel like you are being really "organized" or something but you are just creating a gross shape on the screen that is hard to read. + - Nested navigation in UIs is a bad idea too, keep things as flat as possible. + - Nesting tons of folders in your source code is also not helpful. +2. **Since we need to have more items, here's another one.** + - I'm not sure if we'll bother styling more than two levels deep. + - Two is already too much, three is guaranteed to be a bad idea. + - If you nest four levels deep you belong in prison. +3. **Two items isn't really a list, three is good though.** + - Again please don't nest lists if you want people to actually read your content. + - Nobody wants to look at this. + - I'm upset that we even have to bother styling this. + +The most annoying thing about lists in Markdown is that ` ` elements aren't given a child ` ` tag unless there are multiple paragraphs in the list item. That means I have to worry about styling that annoying situation too. + +- **For example, here's another nested list.** + + But this time with a second paragraph. + + - These list items won't have `
` tags + - Because they are only one line each + +- **But in this second top-level list item, they will.** + + This is especially annoying because of the spacing on this paragraph. + + - As you can see here, because I've added a second line, this list item now has a `
` tag. + + This is the second line I'm talking about by the way. + + - Finally here's another list item so it's more like a list. + +- A closing list item, but with no nested list, because why not? + +And finally a sentence to close off this section. + +## There are other elements we need to style + +I almost forgot to mention links, like [this link to the Tailwind CSS website](https://tailwindcss.com). We almost made them blue but that's so yesterday, so we went with dark gray, feels edgier. + +We even included table styles, check it out: + +| Wrestler | Origin | Finisher | +| ----------------------- | ------------ | ------------------ | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +We also need to make sure inline code looks good, like if I wanted to talk about `` elements or tell you the good news about `@tailwindcss/typography`. + +### Sometimes I even use `code` in headings + +Even though it's probably a bad idea, and historically I've had a hard time making it look good. This _"wrap the code blocks in backticks"_ trick works pretty well though really. + +Another thing I've done in the past is put a `code` tag inside of a link, like if I wanted to tell you about the [`tailwindcss/docs`](https://github.com/tailwindcss/docs) repository. I don't love that there is an underline below the backticks but it is absolutely not worth the madness it would require to avoid it. + +#### We haven't used an `h4` yet + +But now we have. Please don't use `h5` or `h6` in your content, Medium only supports two heading levels for a reason, you animals. I honestly considered using a `before` pseudo-element to scream at you if you use an `h5` or `h6`. + +We don't style them at all out of the box because `h4` elements are already so small that they are the same size as the body copy. What are we supposed to do with an `h5`, make it _smaller_ than the body copy? No thanks. + +### We still need to think about stacked headings though. + +#### Let's make sure we don't screw that up with `h4` elements, either. + +Phew, with any luck we have styled the headings above this text and they look pretty good. + +Let's add a closing paragraph here so things end with a decently sized block of text. I can't explain why I want things to end that way but I have to assume it's because I think things will look weird or unbalanced if there is a heading too close to the end of the document. + +What I've written here is probably long enough, but adding this final sentence can't hurt. + +## GitHub Flavored Markdown + +I've also added support for GitHub Flavored Mardown using `remark-gfm`. + +With `remark-gfm`, we get a few extra features in our markdown. Example: autolink literals. + +A link like www.example.com or https://example.com would automatically be converted into an `a` tag. + +This works for email links too: contact@example.com. diff --git a/apps/www/src/components/content/templete-blog/server-client-components.mdx b/apps/www/src/components/content/templete-blog/server-client-components.mdx new file mode 100644 index 0000000..f71d58c --- /dev/null +++ b/apps/www/src/components/content/templete-blog/server-client-components.mdx @@ -0,0 +1,219 @@ +--- +title: Server and Client Components +description: React Server Components allow developers to build applications that span the server and client. +image: /images/blog/blog-post-4.jpg +date: "2023-01-08" +authors: + - shadcn +--- + +
+ The text below is from the [Tailwind + CSS](https://play.tailwindcss.com/uj1vGACRJA?layout=preview) docs. I copied it + here to test the markdown styles. **Tailwind is awesome. You should use it.** + + +Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS. + +By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. + +We get lots of complaints about it actually, with people regularly asking us things like: + +> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? +> We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either โ you want them to look _awesome_, not awful. + +The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. + +It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: + +```html ++ +``` + +For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). + +--- + +## What to expect from here on out + +What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. + +It's important to cover all of these use cases for a few reasons: + +1. We want everything to look good out of the box. +2. Really just the first reason, that's the whole point of the plugin. +3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. + +Now we're going to try out another header style. + +### Typography should be easy + +So that's a header for you โ with any luck if we've done our job correctly that will look pretty reasonable. + +Something a wise person once told me about typography is: + +> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. + +It's probably important that images look okay here by default as well: + +Garlic bread with cheese: What the science tells us
++ For years parents have espoused the health benefits of eating garlic bread + with cheese to their children, with the food earning such an iconic status + in our culture that kids will often dress up as warm, cheesy loaf for + Halloween. +
++ But a recent study shows that the celebrated appetizer may be linked to a + series of rabies cases springing up around the country. +
++ +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. + +Now I'm going to show you an example of an unordered list to make sure that looks good, too: + +- So here is the first item in this list. +- In this example we're keeping the items short. +- Later, we'll use longer, more complex list items. + +And that's the end of this section. + +## What if we stack headings? + +### We should make sure that looks good, too. + +Sometimes you have headings directly underneath each other. In those cases you often have to undo the top margin on the second heading because it usually looks better for the headings to be closer together than a paragraph followed by a heading should be. + +### When a heading comes after a paragraph โฆ + +When a heading comes after a paragraph, we need a bit more space, like I already mentioned above. Now let's see what a more complex list would look like. + +- **I often do this thing where list items have headings.** + + For some reason I think this looks cool which is unfortunate because it's pretty annoying to get the styles right. + + I often have two or three paragraphs in these list items, too, so the hard part is getting the spacing between the paragraphs, list item heading, and separate list items to all make sense. Pretty tough honestly, you could make a strong argument that you just shouldn't write this way. + +- **Since this is a list, I need at least two items.** + + I explained what I'm doing already in the previous list item, but a list wouldn't be a list if it only had one item, and we really want this to look realistic. That's why I've added this second list item so I actually have something to look at when writing the styles. + +- **It's not a bad idea to add a third item either.** + + I think it probably would've been fine to just use two items but three is definitely not worse, and since I seem to be having no trouble making up arbitrary things to type, I might as well include it. + +After this sort of list I usually have a closing statement or paragraph, because it kinda looks weird jumping right to a heading. + +## Code should look okay by default. + +I think most people are going to use [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/) or something if they want to style their code blocks but it wouldn't hurt to make them look _okay_ out of the box, even with no syntax highlighting. + +Here's what a default `tailwind.config.js` file looks like at the time of writing: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +Hopefully that looks good enough to you. + +### What about nested lists? + +Nested lists basically always look bad which is why editors like Medium don't even let you do it, but I guess since some of you goofballs are going to do it we have to carry the burden of at least making it work. + +1. **Nested lists are rarely a good idea.** + - You might feel like you are being really "organized" or something but you are just creating a gross shape on the screen that is hard to read. + - Nested navigation in UIs is a bad idea too, keep things as flat as possible. + - Nesting tons of folders in your source code is also not helpful. +2. **Since we need to have more items, here's another one.** + - I'm not sure if we'll bother styling more than two levels deep. + - Two is already too much, three is guaranteed to be a bad idea. + - If you nest four levels deep you belong in prison. +3. **Two items isn't really a list, three is good though.** + - Again please don't nest lists if you want people to actually read your content. + - Nobody wants to look at this. + - I'm upset that we even have to bother styling this. + +The most annoying thing about lists in Markdown is that ` ` elements aren't given a child ` ` tag unless there are multiple paragraphs in the list item. That means I have to worry about styling that annoying situation too. + +- **For example, here's another nested list.** + + But this time with a second paragraph. + + - These list items won't have `
` tags + - Because they are only one line each + +- **But in this second top-level list item, they will.** + + This is especially annoying because of the spacing on this paragraph. + + - As you can see here, because I've added a second line, this list item now has a `
` tag. + + This is the second line I'm talking about by the way. + + - Finally here's another list item so it's more like a list. + +- A closing list item, but with no nested list, because why not? + +And finally a sentence to close off this section. + +## There are other elements we need to style + +I almost forgot to mention links, like [this link to the Tailwind CSS website](https://tailwindcss.com). We almost made them blue but that's so yesterday, so we went with dark gray, feels edgier. + +We even included table styles, check it out: + +| Wrestler | Origin | Finisher | +| ----------------------- | ------------ | ------------------ | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +We also need to make sure inline code looks good, like if I wanted to talk about `` elements or tell you the good news about `@tailwindcss/typography`. + +### Sometimes I even use `code` in headings + +Even though it's probably a bad idea, and historically I've had a hard time making it look good. This _"wrap the code blocks in backticks"_ trick works pretty well though really. + +Another thing I've done in the past is put a `code` tag inside of a link, like if I wanted to tell you about the [`tailwindcss/docs`](https://github.com/tailwindcss/docs) repository. I don't love that there is an underline below the backticks but it is absolutely not worth the madness it would require to avoid it. + +#### We haven't used an `h4` yet + +But now we have. Please don't use `h5` or `h6` in your content, Medium only supports two heading levels for a reason, you animals. I honestly considered using a `before` pseudo-element to scream at you if you use an `h5` or `h6`. + +We don't style them at all out of the box because `h4` elements are already so small that they are the same size as the body copy. What are we supposed to do with an `h5`, make it _smaller_ than the body copy? No thanks. + +### We still need to think about stacked headings though. + +#### Let's make sure we don't screw that up with `h4` elements, either. + +Phew, with any luck we have styled the headings above this text and they look pretty good. + +Let's add a closing paragraph here so things end with a decently sized block of text. I can't explain why I want things to end that way but I have to assume it's because I think things will look weird or unbalanced if there is a heading too close to the end of the document. + +What I've written here is probably long enough, but adding this final sentence can't hurt. + +## GitHub Flavored Markdown + +I've also added support for GitHub Flavored Mardown using `remark-gfm`. + +With `remark-gfm`, we get a few extra features in our markdown. Example: autolink literals. + +A link like www.example.com or https://example.com would automatically be converted into an `a` tag. + +This works for email links too: contact@example.com. diff --git a/apps/www/src/components/dashboard/EventsDashboard.tsx b/apps/www/src/components/dashboard/EventsDashboard.tsx new file mode 100644 index 0000000..fec5c60 --- /dev/null +++ b/apps/www/src/components/dashboard/EventsDashboard.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState } from "react"; + +import { AddApiKeyButton } from "@/components/buttons/AddApiKeyButton"; +import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; + +import { DocsButton } from "../buttons/DocsButton"; +import { EventDashboardDetailsSheet } from "./EventsDashboardComponents/EventDashboardDetailsSheet"; +import EventsDashboardCards from "./EventsDashboardComponents/EventsDashboardCards"; +import EventsDashboardDetails from "./EventsDashboardComponents/EventsDashboardDetails"; +import EventsDashboardTable from "./EventsDashboardComponents/EventsDashboardTable"; + +export default function EventsDashboard({ events, eventStats }) { + const [selectedEventId, setSelectedEventId] = useState(events[0]?.id); + + const selectedEvent = events.find((event) => event.id === selectedEventId); + + return ( +
+ + ); +} diff --git a/apps/www/src/components/dashboard/EventsDashboardComponents/EventDashboardDetailsSheet.tsx b/apps/www/src/components/dashboard/EventsDashboardComponents/EventDashboardDetailsSheet.tsx new file mode 100644 index 0000000..0949d63 --- /dev/null +++ b/apps/www/src/components/dashboard/EventsDashboardComponents/EventDashboardDetailsSheet.tsx @@ -0,0 +1,213 @@ +"use client"; + +import Link from "next/link"; +import { createEvent } from "@/actions/create-event"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@dingify/ui/components/button"; +import { Checkbox } from "@dingify/ui/components/checkbox"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@dingify/ui/components/form"; +import { Input } from "@dingify/ui/components/input"; +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@dingify/ui/components/sheet"; +import { toast } from "sonner"; + +// Define the validation schema +const FormSchema = z.object({ + channel: z.string().min(1, "Channel is required"), + name: z.string().min(1, "Name is required"), + event: z.string().min(1, "Event is required"), + user_id: z.string().min(1, "User ID is required"), + icon: z.string().min(1, "Icon is required"), + notify: z.boolean(), +}); + +export function EventDashboardDetailsSheet() { + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + channel: "", + name: "", + event: "", + user_id: "", + icon: "", + notify: true, + }, + }); + + const onSubmit = async (data) => { + console.log("Form data to submit:", data); // Log form dat + try { + const result = await createEvent(data); + if (result.success) { + toast.message( +++++ {events.length > 0 ? ( ++ + ) : ( + + + )} ++ There are no events ++ You need to create an event first to see it here + ++++ + +++ + Event Created. +); + console.log("Event created:", result.event); // Log the created event + // Optionally refresh the page or clear the form + form.reset(); + } + } catch (error) { + toast.error("There was an error creating the event."); + console.error("Error creating event:", error); // Log any error + } + }; + + return ( ++++ {JSON.stringify(data, null, 2)} +
++ + ); +} diff --git a/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardCards.tsx b/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardCards.tsx new file mode 100644 index 0000000..802355c --- /dev/null +++ b/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardCards.tsx @@ -0,0 +1,78 @@ +import { Button } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { Progress } from "@dingify/ui/components/progress"; + +import { CreateEventButton } from "@/components/buttons/CreatEventButton"; + +import { EventDashboardDetailsSheet } from "./EventDashboardDetailsSheet"; + +export default function EventsDashboardCards({ eventStats }) { + return ( + <> ++ + ++ ++ + + +Create Event ++ Fill in the details to create a new event. + ++ ++ +Your Events ++ Introducing Our Dynamic Orders Dashboard for Seamless Management and + Insightful Analysis. + ++ {/* +*/} + + + ++ +This Week ++ {eventStats.currentWeekEvents} + ++ ++ {eventStats.weeklyChange >= 0 + ? `+${eventStats.weeklyChange.toFixed(2)}%` + : `${eventStats.weeklyChange.toFixed(2)}%`}{" "} + from last week +++ + ++ + > + ); +} diff --git a/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardDetails.tsx b/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardDetails.tsx new file mode 100644 index 0000000..20ad182 --- /dev/null +++ b/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardDetails.tsx @@ -0,0 +1,254 @@ +import { useRouter } from "next/navigation"; +import { deleteEvent } from "@/actions/delete-event"; +import { format } from "date-fns"; +import { File, Pencil, Trash } from "lucide-react"; +import { toast } from "sonner"; + +import { Badge } from "@dingify/ui/components/badge"; +import { Button } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; +import { Separator } from "@dingify/ui/components/separator"; + +import { ViewDetailsButton } from "@/components/buttons/ViewDetailsButton"; +import { UserBadge } from "@/components/UserBadge"; + +export default function EventsDashboardDetails({ event }) { + const router = useRouter(); + + const handleDelete = async () => { + try { + await deleteEvent(event.id); + toast.success("The event has been deleted successfully."); + router.refresh(); + } catch (error) { + toast.error("There was an error deleting the event."); + console.error("Error deleting event:", error); + } + }; + + const handleUserClick = (customerId) => { + router.push(`dashboard/users/${customerId}`); + }; + + return ( + <> ++ +This Month ++ {eventStats.currentMonthEvents} + ++ ++ {eventStats.monthlyChange >= 0 + ? `+${eventStats.monthlyChange.toFixed(2)}%` + : `${eventStats.monthlyChange.toFixed(2)}%`}{" "} + from last month +++ + ++ + > + ); +} + +function TruckIcon(props) { + return ( + + ); +} + +function MoveVerticalIcon(props) { + return ( + + ); +} + +function InfoIcon(props) { + return ( + + ); +} diff --git a/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardTable.tsx b/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardTable.tsx new file mode 100644 index 0000000..7049aae --- /dev/null +++ b/apps/www/src/components/dashboard/EventsDashboardComponents/EventsDashboardTable.tsx @@ -0,0 +1,194 @@ +import { useRouter } from "next/navigation"; + +import { Badge } from "@dingify/ui/components/badge"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@dingify/ui/components/table"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@dingify/ui/components/tabs"; + +import { UserBadge } from "@/components/UserBadge"; + +export default function EventsDashboardTable({ + events, + setSelectedEventId, + selectedEventId, +}) { + const router = useRouter(); + + const handleUserClick = (customerId) => { + router.push(`dashboard/users/${customerId}`); + }; + + return ( + <> ++ ++++ {event?.name} + + ++ Date:{" "} + {event?.createdAt ? format(event?.createdAt, "MM/dd/yyyy") : ""} + ++++ + ++ + ++ ++ ++ Edit + + ++ Export + + + ++ Delete + + +++Event Details++
+- + Event Type + {event?.name} +
+- + Channel + +
++ {event?.channel ? event?.channel.name : "No Channel"} + + +- + Project + +
++ {event?.channel && event?.channel.project + ? event?.channel.project.name + : "No Project"} + + ++ +
+- + User + +
++ + - + Description + {event?.name} +
+- + Icon + {event?.icon} +
+- + Notify + +
++ {event?.notify.toString()} + + ++ ++Meta Tags++
+++- Plan
+- +
++ {event?.tags.plan} + +++- Cycle
+- +
++ {event?.tags.cycle} + ++ ++ Updated + {/* */} +++ + > + ); +} + +function FileIcon(props) { + return ( + + ); +} + +function ListFilterIcon(props) { + return ( + + ); +} diff --git a/apps/www/src/components/dashboard/beam-section.tsx b/apps/www/src/components/dashboard/beam-section.tsx new file mode 100644 index 0000000..ed00950 --- /dev/null +++ b/apps/www/src/components/dashboard/beam-section.tsx @@ -0,0 +1,40 @@ +"use client"; + +import React, { forwardRef, useRef } from "react"; + +import { BentoSectionLanding } from "../ui/bento-section-landing"; + +export function BeamSection() { + const containerRef = useRef+ {/*++ */} + {/*Week +Month +Year ++*/} ++ + ++ + ++ +Filter by ++ + Fulfilled + +Declined +Refunded ++ ++ ++ +Events ++ Monitor your app's real-time events. + ++ ++
++ ++ +Channel +Name ++ UserID + +Icon +Notify ++ {events.map((event) => ( + +setSelectedEventId(event.id)} + className={ + selectedEventId === event.id ? "bg-accent" : "" + } + > + + ))} ++ {event.channel && event.channel.name && ( + ++ {event.channel.name} ++ )} + {event.channel && + event.channel.project && + event.channel.project.name && ( ++ {event.channel.project.name} ++ )} ++ {event.name} + ++ ++ + {event.icon} + ++ ++ {event.notify.toString()} + +(null); + + return ( + + + ); +} diff --git a/apps/www/src/components/dashboard/bento-section.tsx b/apps/www/src/components/dashboard/bento-section.tsx new file mode 100644 index 0000000..7653e71 --- /dev/null +++ b/apps/www/src/components/dashboard/bento-section.tsx @@ -0,0 +1,80 @@ +"use client"; + +import React, { forwardRef, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +import { AnimatedBeam } from "../animate-beam"; +import { Icons } from "../shared/icons"; + +const Circle = forwardRef< + HTMLDivElement, + { className?: string; children?: React.ReactNode } +>(({ className, children }, ref) => { + return ( +++ ++ {/*++ Pricing +
*/} + ++ Easy to integrate +
+ ++ It only takes a single HTTP request to get started. + Integrate seamlessly with your our SDK and tools to simplify the + process. +
++++++ + {children} ++ ); +}); + +export function BentoSection() { + const containerRef = useRef(null); + const div1Ref = useRef (null); + const div2Ref = useRef (null); + + return ( + + + ); +} diff --git a/apps/www/src/components/dashboard/businessline.tsx b/apps/www/src/components/dashboard/businessline.tsx new file mode 100644 index 0000000..a169b50 --- /dev/null +++ b/apps/www/src/components/dashboard/businessline.tsx @@ -0,0 +1,211 @@ +import Link from "next/link"; + +export function BusinessLine() { + return ( +++ ++ {/*++ Pricing +
*/} + ++ Easy to integrate +
+ ++ Choose an affordable plan that's packed with + the best features for engaging your audience, creating customer + loyalty, and driving sales. +
++ {" "} + {/* Reduced margins */} ++++++ ++++ ++ + ++ + + + ); +} + +const features = [ + { + title: "Next.js 14", + href: "https://nextjs.org/", + icon: ( + + ), + }, + { + title: "Prisma", + href: "https://www.prisma.io/", + icon: ( + + ), + }, + { + title: "PlanetScale", + href: "https://planetscale.com/", + icon: ( + + ), + }, + { + title: "Auth.js", + href: "https://authjs.dev/", + icon: ( + + ), + }, + { + title: "Resend", + href: "https://resend.com/", + icon: ( + + ), + }, + { + title: "shadcn/ui", + href: "https://ui.shadcn.com/", + icon: ( + + ), + }, + { + title: "Stripe", + href: "https://stripe.com/", + icon: ( + + ), + }, +]; diff --git a/apps/www/src/components/dashboard/calltoaction.tsx b/apps/www/src/components/dashboard/calltoaction.tsx new file mode 100644 index 0000000..1dc7b30 --- /dev/null +++ b/apps/www/src/components/dashboard/calltoaction.tsx @@ -0,0 +1,34 @@ +import { Balancer } from "react-wrap-balancer"; + +import { GetStartedButton } from "../buttons/GetStartedButton"; + +const CallToActionComponent = () => { + return ( ++++ The worlds most innovative companies use our app +
+ ++ {features.map((feature) => ( + + {feature.icon} + + ))} ++++ ); +}; + +export default CallToActionComponent; diff --git a/apps/www/src/components/dashboard/charts/AlertsOverviewChart.tsx b/apps/www/src/components/dashboard/charts/AlertsOverviewChart.tsx new file mode 100644 index 0000000..ab1e0c5 --- /dev/null +++ b/apps/www/src/components/dashboard/charts/AlertsOverviewChart.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { ResponsiveLine } from "@nivo/line"; + +export default function AlertsOverviewChart(props) { + const data = [ + { + id: "Critical", + data: [ + { x: "Jan", y: 30 }, + { x: "Feb", y: 60 }, + { x: "Mar", y: 90 }, + { x: "Apr", y: 120 }, + { x: "May", y: 50 }, + { x: "Jun", y: 40 }, + ], + }, + { + id: "Warning", + data: [ + { x: "Jan", y: 50 }, + { x: "Feb", y: 80 }, + { x: "Mar", y: 100 }, + { x: "Apr", y: 110 }, + { x: "May", y: 80 }, + { x: "Jun", y: 100 }, + ], + }, + { + id: "Info", + data: [ + { x: "Jan", y: 20 }, + { x: "Feb", y: 50 }, + { x: "Mar", y: 60 }, + { x: "Apr", y: 70 }, + { x: "May", y: 90 }, + { x: "Jun", y: 120 }, + ], + }, + ]; + + return ( ++
+Boost your productivity today ++
++ Streamline your property listings and client interactions with the + precision of AI. Propwrite delivers a suite of tools that elevate your + efficiency and let you focus on closing deals โ not on paperwork. + ++++ ++ ); +} diff --git a/apps/www/src/components/dashboard/charts/EventsTrendOverTime.tsx b/apps/www/src/components/dashboard/charts/EventsTrendOverTime.tsx new file mode 100644 index 0000000..66ac59d --- /dev/null +++ b/apps/www/src/components/dashboard/charts/EventsTrendOverTime.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { + Bar, + BarChart, + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +const lineChartData = [ + { month: "Jan", events: 400 }, + { month: "Feb", events: 300 }, + { month: "Mar", events: 200 }, + { month: "Apr", events: 278 }, + { month: "May", events: 189 }, + { month: "Jun", events: 239 }, + { month: "Jul", events: 349 }, + { month: "Aug", events: 430 }, + { month: "Sep", events: 480 }, + { month: "Oct", events: 390 }, + { month: "Nov", events: 139 }, + { month: "Dec", events: 240 }, +]; + +const barChartData = [ + { eventType: "Login", count: 120 }, + { eventType: "Logout", count: 98 }, + { eventType: "Purchase", count: 140 }, + { eventType: "Signup", count: 80 }, + { eventType: "Profile Update", count: 150 }, + { eventType: "Password Reset", count: 60 }, +]; + +export default function EventsTrendOverTimeChart( + { + // lineChartData, + // barChartData, + }, +) { + return ( ++ ++ ); +} diff --git a/apps/www/src/components/dashboard/charts/SalesFunnelChart.tsx b/apps/www/src/components/dashboard/charts/SalesFunnelChart.tsx new file mode 100644 index 0000000..6e55dcb --- /dev/null +++ b/apps/www/src/components/dashboard/charts/SalesFunnelChart.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { ResponsiveFunnel } from "@nivo/funnel"; + +export default function SalesFunnelChart(props) { + const data = [ + { + id: "step_sent", + value: 92558, + label: "Sent", + }, + { + id: "step_viewed", + value: 59485, + label: "Viewed", + }, + { + id: "step_clicked", + value: 37627, + label: "Clicked", + }, + { + id: "step_add_to_card", + value: 33080, + label: "Add To Cart", + }, + { + id: "step_purchased", + value: 26116, + label: "Purchased", + }, + ]; + + return ( ++ ++ ++ Monthly Events + ++ +Event Trends Over Time++ Number of events created each month over the last year. +
++++ ++ ++ + + { + if (active && payload && payload.length) { + return ( + ++ ); + } + + return null; + }} + /> + ++++ + Month + + + {payload[0]?.payload.month} + +++ + Events + + + {payload[0]?.value} + +++ + ++ ++ Top Event Types + ++ +Event Type Breakdown++ Number of each event type over the last year. +
++++ ++ ++ + + { + if (active && payload && payload.length) { + return ( + ++ ); + } + + return null; + }} + /> + ++++ + Event Type + + + {payload[0]?.payload.eventType} + +++ + Count + + + {payload[0]?.value} + +++ ++ ); +} diff --git a/apps/www/src/components/dashboard/charts/UserGrowthChart.tsx b/apps/www/src/components/dashboard/charts/UserGrowthChart.tsx new file mode 100644 index 0000000..396d422 --- /dev/null +++ b/apps/www/src/components/dashboard/charts/UserGrowthChart.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +// Placeholder data for the past week +const PREVIOUS_WEEK_DATA = [ + { name: "30th Nov", users: 30 }, + { name: "1st Dec", users: 50 }, + { name: "2nd Dec", users: 45 }, + { name: "3rd Dec", users: 60 }, + { name: "4th Dec", users: 70 }, + { name: "5th Dec", users: 65 }, +]; + +export default function UserGrowthTrend() { + return ( ++ ++ ); +} diff --git a/apps/www/src/components/dashboard/descriptiondisplay.tsx b/apps/www/src/components/dashboard/descriptiondisplay.tsx new file mode 100644 index 0000000..14992cd --- /dev/null +++ b/apps/www/src/components/dashboard/descriptiondisplay.tsx @@ -0,0 +1,96 @@ +import React from "react"; + +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +import GenerateDescriptionButton2 from "../buttons/GenerateDescriptionButton2"; +import NoTextPlaceholder from "../properties/NoTextPlaceholder"; + +const DescriptionDisplay = ({ + descriptionData, + propertyId, + setDescriptionData, +}) => { + // Check if descriptionData is empty + if (!descriptionData) { + return ( ++ ++ ++ User Growth Trend + ++ +Estimated Growth of Users++ Growth trend over the past week. +
++++ ++ ++ ++ ++ + + + + + + + ); + } + + let parsedData; + try { + // Attempt to parse the JSON string in descriptionData + parsedData = JSON.parse(descriptionData.replace(/```json\n|\n```/g, "")); + // parsedData = JSON.parse("invalid json"); // For testing error message + } catch (error) { + console.error( + `Error parsing description data for property ID ${propertyId}:`, + error + ); + + // Render a user-friendly error message with a retry option + return ( + + + ); + } + + // Function to recursively render content + const renderContent = (data, isNested = false) => { + if (typeof data === "object" && !Array.isArray(data) && data !== null) { + return Object.entries(data).map(([key, value], index) => ( ++ ++ We encountered an issue displaying this property description. +
++ Please try again or contact support if the issue persists. +
++ ++ ++ )); + } else if (Array.isArray(data)) { + return data.map((item, index) => ( +{key}
+ {renderContent(value, true)} ++ {item.title &&+ )); + } else { + return{item.title}
} + {renderContent(item.details || item, true)} +{data}
; + } + }; + + // Render the parsed content + return ( ++ + ); +}; + +export default DescriptionDisplay; diff --git a/apps/www/src/components/dashboard/featuresection1.tsx b/apps/www/src/components/dashboard/featuresection1.tsx new file mode 100644 index 0000000..36ab7c3 --- /dev/null +++ b/apps/www/src/components/dashboard/featuresection1.tsx @@ -0,0 +1,91 @@ +import { + ArrowPathIcon, + CloudArrowUpIcon, + Cog6ToothIcon, + FingerPrintIcon, + LockClosedIcon, + ServerIcon, +} from "@heroicons/react/20/solid"; +import { Balancer } from "react-wrap-balancer"; + +const FeatureSection1 = () => { + const features = [ + { + icon: CloudArrowUpIcon, + title: "Instant Listing Drafts", + description: + "Harness AI to generate captivating property listings instantly. Save time and attract more buyers with eloquent, detail-rich descriptions that stand out.", + }, + { + icon: LockClosedIcon, + title: "Secure Data Handling", + description: + "With our robust SSL encryption, your sensitive property data and client information are safeguarded at every step.", + }, + { + icon: ArrowPathIcon, + title: "Effortless Organization", + description: + "Manage queues of properties with ease. Our system simplifies task management, making follow-ups and updates a breeze.", + }, + // Add the rest of your features here + ]; + + return ( +{renderContent(parsedData)} ++ ++ ++ ); +}; + +export default FeatureSection1; diff --git a/apps/www/src/components/dashboard/feautressection.tsx b/apps/www/src/components/dashboard/feautressection.tsx new file mode 100644 index 0000000..f5fe5ee --- /dev/null +++ b/apps/www/src/components/dashboard/feautressection.tsx @@ -0,0 +1,122 @@ +import { + ArrowPathIcon, + CloudArrowUpIcon, + Cog6ToothIcon, + FingerPrintIcon, + LockClosedIcon, + ServerIcon, +} from "@heroicons/react/20/solid"; +import { Balancer } from "react-wrap-balancer"; + +const features = [ + { + name: "Revolutionizing Real Estate: AI-Powered Listings", + description: + "Instantly draft standout property listings with our advanced AI editor, designed to captivate and attract potential buyers through eloquent, detail-rich descriptions.", + icon: CloudArrowUpIcon, + }, + { + name: "Elevate Your Realty Game with Propwrite", + description: + "Empower your real estate business with Propwrite's SSL encryption, ensuring that all client information and property data are secure and protected.", + icon: LockClosedIcon, + }, + { + name: "Next-Gen Property Management: AI Meets Real Estate", + description: + "Our platform simplifies property management by automating follow-ups and updates, freeing you to focus on what you do best โ closing deals.", + icon: ArrowPathIcon, + }, + { + name: "AI-Enhanced Listings for Smart Agents", + description: + "Stay ahead of security threats with real-time monitoring and automatic updates, safeguarding your listings with the latest in AI technology.", + icon: FingerPrintIcon, + }, + { + name: "Maximize Sales with Intelligent Real Estate Solutions", + description: + "Integrate Propwrite's comprehensive API with your current tools for a seamless experience that enhances your workflow and maximizes efficiency.", + icon: Cog6ToothIcon, + }, + { + name: "Unlock Real Estate Potential with AI Efficiency", + description: + "Ensure your listings are always current and never lost with our reliable backup solutions, providing peace of mind and data security.", + icon: ServerIcon, + }, +]; + +export default function Featuressection() { + return ( ++++++
+Streamlined Real Estate Efficiency ++
++ Redefine property listing management with state-of-the-art AI, + delivering unparalleled efficiency and precision. + +++{/* ... */}++++ {features.map((feature, index) => ( + + ))} +
+++ ); +} diff --git a/apps/www/src/components/dashboard/generatedtext.tsx b/apps/www/src/components/dashboard/generatedtext.tsx new file mode 100644 index 0000000..7401885 --- /dev/null +++ b/apps/www/src/components/dashboard/generatedtext.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useState } from "react"; + +import DescriptionDisplay from "./descriptiondisplay"; + +const Generatedtext = ({ propertyId, descriptionData }) => { + const [localDescriptionData, setLocalDescriptionData] = + useState(descriptionData); + + return ( ++++++ + Propwrite + +
+ ++
+ +Streamlined Real Estate Efficiency ++
++ Propwrite redefines property listing management with + state-of-the-art AI, delivering unparalleled efficiency and + precision. Experience the future of real estateโwhere technology + enhances every transaction, every client interaction, and every + sale + ++++ ++ ++++ {features.map((feature) => ( +
+++ ))} +- +
+ +{feature.name} +- +
+{feature.description} +++ ); +}; + +export default Generatedtext; diff --git a/apps/www/src/components/dashboard/header.tsx b/apps/www/src/components/dashboard/header.tsx new file mode 100644 index 0000000..b1a4146 --- /dev/null +++ b/apps/www/src/components/dashboard/header.tsx @@ -0,0 +1,21 @@ +interface DashboardHeaderProps { + heading: string; + text?: string; + children?: React.ReactNode; +} + +export function DashboardHeader({ + heading, + text, + children, +}: DashboardHeaderProps) { + return ( ++ ++ ); +} diff --git a/apps/www/src/components/dashboard/herosection.tsx b/apps/www/src/components/dashboard/herosection.tsx new file mode 100644 index 0000000..afa31b5 --- /dev/null +++ b/apps/www/src/components/dashboard/herosection.tsx @@ -0,0 +1,82 @@ +"use client"; + +import Link from "next/link"; +import Balancer from "react-wrap-balancer"; + +import { buttonVariants } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +import { cn } from "../../lib/utils"; +import { GetStartedButton } from "../buttons/GetStartedButton"; +import { Icons } from "../shared/icons"; +import AlertsOverviewChart from "./charts/AlertsOverviewChart"; + +export default function HeroSection() { + return ( +++ {children} +{heading}
+ {text &&{text}
} ++ + ); +} diff --git a/apps/www/src/components/dashboard/herosection2.tsx b/apps/www/src/components/dashboard/herosection2.tsx new file mode 100644 index 0000000..85d607d --- /dev/null +++ b/apps/www/src/components/dashboard/herosection2.tsx @@ -0,0 +1,87 @@ +"use client"; + +import Link from "next/link"; +import Balancer from "react-wrap-balancer"; + +import { buttonVariants } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +import { getGreeting } from "@/lib/utils"; + +import { cn } from "../../lib/utils"; +import { GetStartedButton } from "../buttons/GetStartedButton"; +import SalesFunnelChart from "./charts/SalesFunnelChart"; + +export default function HeroSection2() { + const greeting = getGreeting(); + return ( ++++++++++
+Real-Time Monitoring with Dingify ++ Unlock the power of seamless real-time monitoring that + captivates your audience and drives results. +
++++ + + + Explore{" "} + Dingify{" "} +
+ ++ ++ +Alerts Overview ++ A graph showing the different alert types over time. + ++ ++ + + ); +} diff --git a/apps/www/src/components/dashboard/shell.tsx b/apps/www/src/components/dashboard/shell.tsx new file mode 100644 index 0000000..62127e9 --- /dev/null +++ b/apps/www/src/components/dashboard/shell.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +type DashboardShellProps = React.HTMLAttributes+++++ + ++ +Sales Funnel Overview ++ A graph showing conversion rates over different steps. + ++ ++ +++++
+Track your Important Events this {greeting} ++ Unlock the power of seamless real-time monitoring that + captivates your audience and drives results. +
++++ + Explore Dingity + + ; + +export function DashboardShell({ + children, + className, + ...props +}: DashboardShellProps) { + return ( + + {children} ++ ); +} diff --git a/apps/www/src/components/dashboard/test.tsx b/apps/www/src/components/dashboard/test.tsx new file mode 100644 index 0000000..4b00bad --- /dev/null +++ b/apps/www/src/components/dashboard/test.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { Button } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { Input } from "@dingify/ui/components/input"; +import { Label } from "@dingify/ui/components/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@dingify/ui/components/select"; +import { Textarea } from "@dingify/ui/components/textarea"; + +export function InputRightSideTest() { + return ( ++ + ); +} diff --git a/apps/www/src/components/dashboard/updatepropertyform2.tsx b/apps/www/src/components/dashboard/updatepropertyform2.tsx new file mode 100644 index 0000000..bf0edd6 --- /dev/null +++ b/apps/www/src/components/dashboard/updatepropertyform2.tsx @@ -0,0 +1,114 @@ +"use client"; + +import React, { useState } from "react"; +import { updatePropertyDetails } from "@/actions/update-property-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 as Spinner } from "lucide-react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { Input } from "@dingify/ui/components//input"; +import { Label } from "@dingify/ui/components//label"; +import { Textarea } from "@dingify/ui/components//textarea"; +import { Button } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { toast } from "sonner"; + +// Define your schema as per your requirements +const propertyFormSchema = z.object({ + address: z.string(), + description: z.string().optional(), + pris: z.string(), + p_rom: z.string(), + bra: z.string(), + // ... add other fields as necessary +}); + +export function UpdatePropertyForm2({ defaultValues, propertyId }) { + const [isLoading, setIsLoading] = useState(false); // Add this line + const form = useForm({ + resolver: zodResolver(propertyFormSchema), + defaultValues: defaultValues, + }); + + const onSubmit = async (data) => { + setIsLoading(true); // Start loading + try { + const response = await updatePropertyDetails(propertyId, data); + if (response.success) { + toast.success("Property details updated successfully."); + } else { + throw new Error(response.error); + } + } catch (error) { + toast.error("Failed to update property details."); + } + setIsLoading(false); // Stop loading + }; + + return ( ++ +Report an issue ++ What area are you having problems with? + ++ ++++ + +++ + +++ + +++ + +++ + + ++ + ); +} diff --git a/apps/www/src/components/docs/page-header.tsx b/apps/www/src/components/docs/page-header.tsx new file mode 100644 index 0000000..17cea34 --- /dev/null +++ b/apps/www/src/components/docs/page-header.tsx @@ -0,0 +1,25 @@ +import { cn } from "@/lib/utils"; + +interface DocsPageHeaderProps extends React.HTMLAttributes+ +Update Property Details +Update the appraisal report ++ + ++ + +{ + heading: string; + text?: string; +} + +export function DocsPageHeader({ + heading, + text, + className, + ...props +}: DocsPageHeaderProps) { + return ( + <> + +++ {heading} +
+ {text &&{text}
} +
+ > + ); +} diff --git a/apps/www/src/components/docs/pager.tsx b/apps/www/src/components/docs/pager.tsx new file mode 100644 index 0000000..b7f409c --- /dev/null +++ b/apps/www/src/components/docs/pager.tsx @@ -0,0 +1,64 @@ +import type { Doc } from "contentlayer/generated"; +import Link from "next/link"; +import { Icons } from "@/components/shared/icons"; +import { docsConfig } from "@/config/docs"; +import { cn } from "@/lib/utils"; + +import { buttonVariants } from "@dingify/ui/components/button"; + +interface DocsPagerProps { + doc: Doc; +} + +export function DocsPager({ doc }: DocsPagerProps) { + const pager = getPagerForDoc(doc); + + if (!pager) { + return null; + } + + return ( ++ {pager.prev && ( + ++ ); +} + +export function getPagerForDoc(doc: Doc) { + const flattenedLinks = [null, ...flatten(docsConfig.sidebarNav), null]; + const activeIndex = flattenedLinks.findIndex( + (link) => doc.slug === link?.href + ); + const prev = activeIndex !== 0 ? flattenedLinks[activeIndex - 1] : null; + const next = + activeIndex !== flattenedLinks.length - 1 + ? flattenedLinks[activeIndex + 1] + : null; + return { + prev, + next, + }; +} + +export function flatten(links: { items? }[]) { + return links.reduce((flat, link) => { + return flat.concat(link.items ? flatten(link.items) : link); + }, []); +} diff --git a/apps/www/src/components/docs/search.tsx b/apps/www/src/components/docs/search.tsx new file mode 100644 index 0000000..dc107da --- /dev/null +++ b/apps/www/src/components/docs/search.tsx @@ -0,0 +1,34 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; + +import { Input } from "@dingify/ui/components/input"; +import { toast } from "sonner"; + +type DocsSearchProps = React.HTMLAttributes+ {pager.prev.title} + + )} + {pager.next && ( + + {pager.next.title} + + + )} + ; + +export function DocsSearch({ className, ...props }: DocsSearchProps) { + function onSubmit(event: React.SyntheticEvent) { + event.preventDefault(); + + return toast.info("We're still working on the search.") + } + + return ( + + ); +} diff --git a/apps/www/src/components/docs/sidebar-nav.tsx b/apps/www/src/components/docs/sidebar-nav.tsx new file mode 100644 index 0000000..6a37a4b --- /dev/null +++ b/apps/www/src/components/docs/sidebar-nav.tsx @@ -0,0 +1,69 @@ +"use client"; + +import type { SidebarNavItem } from "@/types"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; + +export interface DocsSidebarNavProps { + items: SidebarNavItem[]; +} + +export function DocsSidebarNav({ items }: DocsSidebarNavProps) { + const pathname = usePathname(); + + return items.length ? ( + + {items.map((item) => ( ++ ) : null; +} + +interface DocsSidebarNavItemsProps { + items: SidebarNavItem[]; + pathname: string | null; +} + +export function DocsSidebarNavItems({ + items, + pathname, +}: DocsSidebarNavItemsProps) { + return items.length ? ( +++ ))} ++ {item.title} +
+ {item.items ? ( ++ ) : null} + + {items.map((item, index) => + !item.disabled && item.href ? ( + + {item.title} + + ) : ( + + {item.title} + + ) + )} ++ ) : null; +} diff --git a/apps/www/src/components/forms/billing-form-button.tsx b/apps/www/src/components/forms/billing-form-button.tsx new file mode 100644 index 0000000..3413437 --- /dev/null +++ b/apps/www/src/components/forms/billing-form-button.tsx @@ -0,0 +1,53 @@ +"use client"; + +import type { SubscriptionPlan, UserSubscriptionPlan } from "@/types"; +import { useTransition } from "react"; +import { generateUserStripe } from "@/actions/generate-user-stripe"; +import { Icons } from "@/components/shared/icons"; + +import { Button } from "@dingify/ui/components/button"; + +interface BillingFormButtonProps { + offer: SubscriptionPlan; + subscriptionPlan: UserSubscriptionPlan; + year: boolean; +} + +export function BillingFormButton({ + year, + offer, + subscriptionPlan, +}: BillingFormButtonProps) { + const [isPending, startTransition] = useTransition(); + const generateUserStripeSession = generateUserStripe.bind( + null, + // @ts-expect-error + offer.stripeIds[year ? "yearly" : "monthly"] + ); + + const stripeSessionAction = () => + // @ts-expect-error + startTransition(async () => await generateUserStripeSession()); + + return ( + + ); +} diff --git a/apps/www/src/components/forms/language-form2.tsx b/apps/www/src/components/forms/language-form2.tsx new file mode 100644 index 0000000..2ad0267 --- /dev/null +++ b/apps/www/src/components/forms/language-form2.tsx @@ -0,0 +1,109 @@ +"use client"; + +import React, { useState } from "react"; +import { updateUserLanguage } from "@/actions/update-language"; +import { Icons } from "@/components/shared/icons"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { Button, buttonVariants } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@dingify/ui/components/select"; +import { toast } from "sonner"; + +const languageSchema = z.object({ + language: z.string({ + required_error: "Language is required", + }), +}); + +export function LanguageForm2({ user }) { + const [selectedLanguage, setSelectedLanguage] = useState( + user.language || "english" + ); + const [isPending, setPending] = useState(false); + const form = useForm({ + resolver: zodResolver(languageSchema), + // Remove defaultValues here, we will use useState to manage the select's value + }); + + const { handleSubmit, setValue } = form; + + const onSubmit = async (data) => { + setPending(true); + console.log("Submitted data:", data); // Check what is being submitted + try { + const response = await updateUserLanguage(data.language); + console.log("Update response:", response); // Check the response from the update call + if (response.success) { + toast.success("Language updated successfully."); + } else { + throw new Error(response.error); + } + } catch (error) { + console.error("Update error:", error); // Log any caught errors + toast.error(error.message || "Failed to update language."); + } finally { + setPending(false); + } + }; + + // Use this function to handle the language change + const handleLanguageChange = (language) => { + setSelectedLanguage(language); + setValue("language", language); // Update form value manually + console.log("Language changed to:", language); // Log the change for debugging + }; + + return ( ++ + + ); +} diff --git a/apps/www/src/components/forms/select-input-form.tsx b/apps/www/src/components/forms/select-input-form.tsx new file mode 100644 index 0000000..3ad2505 --- /dev/null +++ b/apps/www/src/components/forms/select-input-form.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { selectOption } from "@/actions/select-option"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { X } from "lucide-react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { + Form, + FormControl, + FormDescription, + FormField, + FormLabel, +} from "@dingify/ui/components//form"; +import { Switch } from "@dingify/ui/components//switch"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { toast } from "sonner"; + +export function SelectInputForm({ options, image }) { + const router = useRouter(); + const [activeSwitch, setActiveSwitch] = useState(null); + + const dynamicSchema = options.reduce((acc, option) => { + acc[option.key] = z.boolean(); + return acc; + }, {}); + + const form = useForm({ + resolver: zodResolver(z.object(dynamicSchema)), + }); + + const handleSwitchChange = async (option) => { + setActiveSwitch(option.label); + + // Get the content of the selected option + const selectedOptionContent = option.description; + + // Call the server action to update the selected option in the database + try { + const result = await selectOption(option.imageId, selectedOptionContent); // Use selectedOptionContent here + if (!result.success) { + // If an error occurs, log it and display a toast notification + console.error("Failed to update the selected option:", result.error); + toast.error("Failed to update the selected option. Please try again."); + } else { + router.refresh(); + console.log("Selected option updated successfully"); + } + } catch (error) { + console.error("An unexpected error occurred:", error); + toast.error("An unexpected error occurred. Please try again."); + } + }; + + const onSubmit = (data) => { + console.log(data); + }; + + return ( + + + ); +} diff --git a/apps/www/src/components/forms/update-property-form.tsx b/apps/www/src/components/forms/update-property-form.tsx new file mode 100644 index 0000000..a7dc966 --- /dev/null +++ b/apps/www/src/components/forms/update-property-form.tsx @@ -0,0 +1,58 @@ +// components/forms/update-property-form.js +"use client"; + +import React from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { Button } from "@dingify/ui/components/button"; +import { + Form, + FormControl, + FormItem, + FormLabel, +} from "@dingify/ui/components/form"; +import { Input } from "@dingify/ui/components/input"; + +const propertyFormSchema = z.object({ + address: z.string(), + description: z.string().optional(), + p_rom: z.string().optional(), + bra: z.string().optional(), + soverom: z.string().optional(), + pris: z.string().optional(), + takst_text: z.string().optional(), +}); + +export function UpdatePropertyForm({ propertyId, defaultValues }) { + const form = useForm({ + resolver: zodResolver(propertyFormSchema), + defaultValues: defaultValues, + }); + + const onSubmit = async (data) => { + // Here you would handle the form submission, + // likely sending the data to your backend to update the property + console.log("Form data submitted:", data); + + // Example of a server action or API call + // await updateProperty(propertyId, data); + }; + + return ( + + + ); +} diff --git a/apps/www/src/components/forms/user-auth-form.tsx b/apps/www/src/components/forms/user-auth-form.tsx new file mode 100644 index 0000000..a175e41 --- /dev/null +++ b/apps/www/src/components/forms/user-auth-form.tsx @@ -0,0 +1,114 @@ +"use client"; + +import type * as z from "zod"; +import * as React from "react"; +import { useSearchParams } from "next/navigation"; +import { Icons } from "@/components/shared/icons"; +import { cn } from "@/lib/utils"; +import { userAuthSchema } from "@/lib/validations/auth"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { signIn } from "next-auth/react"; +import { useForm } from "react-hook-form"; + +import { buttonVariants } from "@dingify/ui/components/button"; +import { Input } from "@dingify/ui/components/input"; +import { Label } from "@dingify/ui/components/label"; +import { toast } from "sonner"; + +interface UserAuthFormProps extends React.HTMLAttributes{ + type?: string; +} + +type FormData = z.infer ; + +export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm ({ + resolver: zodResolver(userAuthSchema), + }); + const [isLoading, setIsLoading] = React.useState (false); + const [isGoogleLoading, setIsGoogleLoading] = React.useState (false); + const searchParams = useSearchParams(); + + async function onSubmit(data: FormData) { + setIsLoading(true); + + const signInResult = await signIn("email", { + email: data.email.toLowerCase(), + redirect: false, + callbackUrl: searchParams.get("from") || "/dashboard", + }); + + setIsLoading(false); + + if (!signInResult?.ok) { + return toast.error("Your sign in request failed. Please try again."); + } + + return toast.success("We sent you a login link. Be sure to check your spam too."); + } + + return ( + + ++ ); +} diff --git a/apps/www/src/components/forms/user-name-form.tsx b/apps/www/src/components/forms/user-name-form.tsx new file mode 100644 index 0000000..a6d9bc1 --- /dev/null +++ b/apps/www/src/components/forms/user-name-form.tsx @@ -0,0 +1,98 @@ +"use client"; + +import type { FormData } from "@/actions/update-user-name"; +import type { User } from "@prisma/client"; +import { useTransition } from "react"; +import { updateUserName } from "@/actions/update-user-name"; +import { Icons } from "@/components/shared/icons"; +import { cn } from "@/lib/utils"; +import { userNameSchema } from "@/lib/validations/user"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +import { buttonVariants } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { Input } from "@dingify/ui/components/input"; +import { Label } from "@dingify/ui/components/label"; +import { toast } from "sonner"; + +interface UserNameFormProps { + user: Pick++ ++ +++ + Or continue with + ++; +} + +export function UserNameForm({ user }: UserNameFormProps) { + const [isPending, startTransition] = useTransition(); + const updateUserNameWithId = updateUserName.bind(null, user.id); + + const { + handleSubmit, + register, + formState: { errors }, + } = useForm ({ + resolver: zodResolver(userNameSchema), + defaultValues: { + name: user.name || "", + }, + }); + + const onSubmit = handleSubmit((data) => { + startTransition(async () => { + const { status } = await updateUserNameWithId(data); + + if (status !== "success") { + toast.error("Your name was not updated. Please try again."); + } else { + toast.success("Your name has been updated."); + } + }); + }); + + return ( + + ); +} diff --git a/apps/www/src/components/landing/Integrations-section-landing.tsx b/apps/www/src/components/landing/Integrations-section-landing.tsx new file mode 100644 index 0000000..5c827c4 --- /dev/null +++ b/apps/www/src/components/landing/Integrations-section-landing.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { + AnimatePresence, + cubicBezier, + motion, + useAnimation, + useInView, +} from "framer-motion"; + +import { Confetti } from "../ui/confetti"; + +const cardImage = [ + { + id: 1, + title: "Tom", + link: "#", + image: "https://avatar.vercel.sh/tom", + }, + { + id: 2, + title: "Emily", + link: "#", + image: "https://avatar.vercel.sh/emily", + }, + { + id: 3, + title: "Chris", + link: "#", + image: "https://avatar.vercel.sh/chris", + }, + { + id: 4, + title: "Sophie", + link: "#", + image: "https://avatar.vercel.sh/sophie", + }, + { + id: 5, + title: "Scott", + link: "#", + image: "https://avatar.vercel.sh/scott", + }, + { + id: 6, + title: "Olivia", + link: "#", + image: "https://avatar.vercel.sh/olivia", + }, + { + id: 7, + title: "Evan", + link: "#", + image: "https://avatar.vercel.sh/evan", + }, + { + id: 8, + title: "Grace", + link: "#", + image: "https://avatar.vercel.sh/grace", + }, + { + id: 9, + title: "Van", + link: "#", + image: "https://avatar.vercel.sh/van", + }, +]; + +export function IntegrationsSectionLanding() { + const containerRef = useRef(null); + const inView = useInView(containerRef, { amount: 0.25 }); + const controls = useAnimation(); + + useEffect(() => { + if (inView) { + controls.start("visible"); + } else { + controls.start("hidden"); + } + }, [inView, controls]); + + const handleConfettiClick = () => { + Confetti({}); + }; + + return ( + ++ ); +} diff --git a/apps/www/src/components/landing/bottom-section-landing.tsx b/apps/www/src/components/landing/bottom-section-landing.tsx new file mode 100644 index 0000000..9d7a1b7 --- /dev/null +++ b/apps/www/src/components/landing/bottom-section-landing.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useRef } from "react"; +import { ArrowRightIcon } from "@radix-ui/react-icons"; +import { useInView } from "framer-motion"; +import { FileInputIcon } from "lucide-react"; + +import { Button } from "@dingify/ui/components/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@dingify/ui/components/command"; + +import { GetStartedButton } from "../buttons/GetStartedButton"; +import { BentoCard } from "../ui/bento-grid"; + +export default function BottomSectionLanding() { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: "-100px" }); + + return ( ++++ Dingify makes it easy to track the journey of your users +
+ + Get started for free + ++ +++++ {cardImage.map((card, index) => ( + ++ + + ))} +{card.title}
+ + Website + ++ + ); +} diff --git a/apps/www/src/components/landing/cta-section.tsx b/apps/www/src/components/landing/cta-section.tsx new file mode 100644 index 0000000..7d79c30 --- /dev/null +++ b/apps/www/src/components/landing/cta-section.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useEffect, useId, useRef, useState } from "react"; +import Link from "next/link"; +import { motion, useAnimation, useInView } from "framer-motion"; +import { + BarChart, + ChevronRight, + File, + Globe, + HeartHandshake, + Navigation, + PieChart, + Plug, + Rss, + Shield, +} from "lucide-react"; + +import { buttonVariants } from "@dingify/ui/components/button"; + +import { cn } from "@/lib/utils"; + +import Marquee from "../ui/marquee"; + +const tiles = [ + { + icon:+ Built for developers +
+
Available today. ++ {/*+*/} + + , // Analytics + bg: ( + + ), + }, + { + icon: , // Infrastructure + bg: ( + + ), + }, + { + icon: , // User Journeys + bg: ( + + ), + }, + { + icon: , // Data Visualization + bg: ( + + ), + }, + { + icon: , // Integrations + bg: ( + + ), + }, + { + icon: , // Additional service + bg: ( + + ), + }, +]; + +const shuffleArray = (array: any[]) => { + let currentIndex = array.length, + randomIndex; + // While there remain elements to shuffle. + while (currentIndex !== 0) { + // Pick a remaining element. + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], + array[currentIndex], + ]; + } + return array; +}; + +const Card = (card: { icon: JSX.Element; bg: JSX.Element }) => { + const id = useId(); + const controls = useAnimation(); + const ref = useRef(null); + const inView = useInView(ref, { once: true }); + + useEffect(() => { + if (inView) { + controls.start({ + opacity: 1, + transition: { delay: Math.random() * 2, ease: "easeOut", duration: 1 }, + }); + } + }, [controls, inView]); + + return ( + + {card.icon} + {card.bg} + + ); +}; + +export default function CallToActionSection() { + const [randomTiles1, setRandomTiles1] = useState([]); + const [randomTiles2, setRandomTiles2] = useState ([]); + const [randomTiles3, setRandomTiles3] = useState ([]); + const [randomTiles4, setRandomTiles4] = useState ([]); + + useEffect(() => { + if (typeof window !== "undefined") { + // Ensures this runs client-side + setRandomTiles1(shuffleArray([...tiles])); + setRandomTiles2(shuffleArray([...tiles])); + setRandomTiles3(shuffleArray([...tiles])); + setRandomTiles4(shuffleArray([...tiles])); + } + }, []); + + return ( + + + ); +} diff --git a/apps/www/src/components/landing/events-section-landing.tsx b/apps/www/src/components/landing/events-section-landing.tsx new file mode 100644 index 0000000..f6ed3cd --- /dev/null +++ b/apps/www/src/components/landing/events-section-landing.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useRef } from "react"; +import { ArrowRightIcon } from "@radix-ui/react-icons"; +import { useInView } from "framer-motion"; +import { FileInputIcon } from "lucide-react"; + +import { Button } from "@dingify/ui/components/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@dingify/ui/components/command"; + +import { BentoCard } from "../ui/bento-grid"; + +export default function EventsSectionLanding() { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: "-100px" }); + + return ( ++++++ + + + + + ++++ ++++ ++ ++ Track every user interaction with ease +
+No credit card required.
+ + Get Started for free ++ + + + ); +} diff --git a/apps/www/src/components/landing/hero-section-new.tsx b/apps/www/src/components/landing/hero-section-new.tsx new file mode 100644 index 0000000..fc4eb99 --- /dev/null +++ b/apps/www/src/components/landing/hero-section-new.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { useRef } from "react"; +import { ArrowRightIcon } from "@radix-ui/react-icons"; +import { useInView } from "framer-motion"; + +import { Button } from "@dingify/ui/components/button"; + +import { BorderBeam } from "../ui/border-beam"; +import TextShimmer from "../ui/text-shimmer"; + +export default function HeroSectionNew() { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: "-100px" }); + return ( ++ The information you need +
+
when you need it. ++++ + + + + } + /> +No results found. ++ +User-123 +Most used events +Analytics +All events +keys.gpg +seed.txt ++ Unlock the power of real-time alerts and monitoring +
+
Monitor potential issues and + opportunities ++ + ); +} diff --git a/apps/www/src/components/landing/hero-section.tsx b/apps/www/src/components/landing/hero-section.tsx new file mode 100644 index 0000000..b4a961a --- /dev/null +++ b/apps/www/src/components/landing/hero-section.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useRef } from "react"; +import { ArrowRightIcon } from "@radix-ui/react-icons"; +import { useInView } from "framer-motion"; + +import { Button } from "@dingify/ui/components/button"; + +import { GetStartedButton } from "../buttons/GetStartedButton"; +import { BorderBeam } from "../ui/border-beam"; +import TextShimmer from "../ui/text-shimmer"; + +export default function HeroSectionNew2() { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: "-100px" }); + return ( ++++ โจ Introducing Magic UI Template{" "} + ++ + Magic UI is the new way +
+
to build landing pages. ++ Beautifully designed, animated components and templates built with +
+ +
Tailwind CSS, React, and Framer + Motion. ++++++ + + + + + ); +} diff --git a/apps/www/src/components/layout/language-modal.tsx b/apps/www/src/components/layout/language-modal.tsx new file mode 100644 index 0000000..f02f7d2 --- /dev/null +++ b/apps/www/src/components/layout/language-modal.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { useState } from "react"; +import { updateUserLanguage } from "@/actions/update-language"; +import { Modal } from "@/components/shared/modal"; +import { useLanguageModal } from "@/hooks/use-language-modal"; + +import { Button } from "@dingify/ui/components/button"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@dingify/ui/components/select"; + +// Add any additional imports for user feedback or styling + +export const LanguageModal = () => { + const languageModal = useLanguageModal(); + const [selectedLanguage, setSelectedLanguage] = useState(""); + + const handleLanguageSubmit = async () => { + try { + const response = await updateUserLanguage(selectedLanguage); + if (response.success) { + console.log("Language update successful"); + // Optionally add user feedback here (like a toast notification) + } else { + console.error("Language update failed:", response.error); + // Optionally add error feedback here + } + } catch (error) { + console.error("Error updating language:", error); + // Optionally add error feedback here + } finally { + languageModal.onClose(); // Close the modal after submission + } + }; + + return ( ++++ โจ Introducing Dingify{" "} + ++ + Dingify is the new way +
+
to monitor you business. ++ Unlock the power of real-time alerts and monitoring +
+
Monitor potental issues and + opportunities ++ +++++ + + + + + ); +}; diff --git a/apps/www/src/components/layout/main-nav.tsx b/apps/www/src/components/layout/main-nav.tsx new file mode 100644 index 0000000..2941e45 --- /dev/null +++ b/apps/www/src/components/layout/main-nav.tsx @@ -0,0 +1,78 @@ +"use client"; + +import type { MainNavItem } from "@/types"; +import * as React from "react"; +import Link from "next/link"; +import { useSelectedLayoutSegment } from "next/navigation"; +import { MobileNav } from "@/components/layout/mobile-nav"; +import { Icons } from "@/components/shared/icons"; +import { siteConfig } from "@/config/site"; +import { cn } from "@/lib/utils"; + +interface MainNavProps { + items?: MainNavItem[]; + children?: React.ReactNode; +} + +export function MainNav({ items, children }: MainNavProps) { + const segment = useSelectedLayoutSegment(); + const [showMobileMenu, setShowMobileMenu] = React.useState+++ {/* Modal header content */} ++Select Language
++ Choose your preferred language for Propwrite. (You can always edit + this in settings) +
++ {/* Language selection dropdown */} + + {/* Submit button */} + ++(false); + + const toggleMobileMenu = () => { + setShowMobileMenu(!showMobileMenu); + }; + + React.useEffect(() => { + const closeMobileMenuOnClickOutside = (event: MouseEvent) => { + if (showMobileMenu) { + setShowMobileMenu(false); + } + }; + + document.addEventListener("click", closeMobileMenuOnClickOutside); + + return () => { + document.removeEventListener("click", closeMobileMenuOnClickOutside); + }; + }, [showMobileMenu]); + + return ( + + ++ ); +} diff --git a/apps/www/src/components/layout/mobile-nav.tsx b/apps/www/src/components/layout/mobile-nav.tsx new file mode 100644 index 0000000..757f934 --- /dev/null +++ b/apps/www/src/components/layout/mobile-nav.tsx @@ -0,0 +1,46 @@ +import type { MainNavItem } from "@/types"; +import * as React from "react"; +import Link from "next/link"; +import { Icons } from "@/components/shared/icons"; +import { siteConfig } from "@/config/site"; +import { useLockBody } from "@/hooks/use-lock-body"; +import { cn } from "@/lib/utils"; + +interface MobileNavProps { + items: MainNavItem[]; + children?: React.ReactNode; +} + +export function MobileNav({ items, children }: MobileNavProps) { + useLockBody(); + + return ( ++ + {siteConfig.name} + + + {items?.length ? ( + + ) : null} + + {showMobileMenu && items && ( + {children} + )} +++ ); +} diff --git a/apps/www/src/components/layout/mode-toggle.tsx b/apps/www/src/components/layout/mode-toggle.tsx new file mode 100644 index 0000000..61c4d2a --- /dev/null +++ b/apps/www/src/components/layout/mode-toggle.tsx @@ -0,0 +1,43 @@ +"use client"; + +import * as React from "react"; +import { Icons } from "@/components/shared/icons"; +import { useTheme } from "next-themes"; + +import { Button } from "@dingify/ui/components/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( ++ +++ {siteConfig.name} + + + {children} + + + ); +} diff --git a/apps/www/src/components/layout/nav.tsx b/apps/www/src/components/layout/nav.tsx new file mode 100644 index 0000000..656450f --- /dev/null +++ b/apps/www/src/components/layout/nav.tsx @@ -0,0 +1,67 @@ +"use client"; + +import type { SidebarNavItem } from "@/types"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { Separator } from "@dingify/ui/components/separator"; + +import { cn } from "@/lib/utils"; +import { Icons } from "@/components/shared/icons"; + +interface DashboardNavProps { + items: SidebarNavItem[]; + slug?: string; +} + +export function DashboardNav({ items, slug }: DashboardNavProps) { + const path = usePathname(); + + if (!items.length) { + return null; + } + + return ( + + ); +} diff --git a/apps/www/src/components/layout/navbar.tsx b/apps/www/src/components/layout/navbar.tsx new file mode 100644 index 0000000..d1b454b --- /dev/null +++ b/apps/www/src/components/layout/navbar.tsx @@ -0,0 +1,72 @@ +"use client"; + +import type { MainNavItem } from "@/types"; +import type { User } from "next-auth"; +import Link from "next/link"; +import useScroll from "@/hooks/use-scroll"; +import { useSigninModal } from "@/hooks/use-signin-modal"; +import { cn } from "@/lib/utils"; + +import { Button, buttonVariants } from "@dingify/ui/components/button"; + +import { MainNav } from "./main-nav"; +import { UserAccountNav } from "./user-account-nav"; + +interface NavBarProps { + user: Pick+ + ++ +setTheme("light")}> + ++ Light + setTheme("dark")}> + ++ Dark + setTheme("system")}> + ++ System + | undefined; + items?: MainNavItem[]; + children?: React.ReactNode; + rightElements?: React.ReactNode; + scroll?: boolean; +} + +export function NavBar({ + user, + items, + children, + rightElements, + scroll = false, +}: NavBarProps) { + const scrolled = useScroll(50); + const signInModal = useSigninModal(); + + return ( + + + ); +} diff --git a/apps/www/src/components/layout/sign-in-modal.tsx b/apps/www/src/components/layout/sign-in-modal.tsx new file mode 100644 index 0000000..4fa8aff --- /dev/null +++ b/apps/www/src/components/layout/sign-in-modal.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useState } from "react"; +import { signIn } from "next-auth/react"; + +import { Button } from "@dingify/ui/components/button"; + +import { siteConfig } from "@/config/site"; +import { useSigninModal } from "@/hooks/use-signin-modal"; +import { Icons } from "@/components/shared/icons"; +import { Modal } from "@/components/shared/modal"; + +export const SignInModal = () => { + const signInModal = useSigninModal(); + const [signInClicked, setSignInClicked] = useState(false); + + return ( +++{children} + ++ {rightElements} + + {/* {!user ? ( + + Login Page + + ) : null} */} + + {user ? ( +++ ) : ( + + )} + + + ); +}; diff --git a/apps/www/src/components/layout/site-footer.tsx b/apps/www/src/components/layout/site-footer.tsx new file mode 100644 index 0000000..b774e9c --- /dev/null +++ b/apps/www/src/components/layout/site-footer.tsx @@ -0,0 +1,145 @@ +import * as React from "react"; +import Link from "next/link"; +import { DiscordLogoIcon, TwitterLogoIcon } from "@radix-ui/react-icons"; + +import { siteConfig } from "@/config/site"; +import { cn } from "@/lib/utils"; +import { ModeToggle } from "@/components/layout/mode-toggle"; +import { Icons } from "@/components/shared/icons"; + +const footerNavs = [ + { + label: "Product", + items: [ + { + href: "/", + name: "Docs", + }, + { + href: "/pricing", + name: "Pricing", + }, + { + href: "/open", + name: "Open Startup", + }, + ], + }, + // { + // label: "Community", + // items: [ + // { + // href: "/", + // name: "Discord", + // }, + // { + // href: "/", + // name: "Twitter", + // }, + // { + // href: "mailto:hello@chatcollect.com", + // name: "Email", + // }, + // ], + // }, + // { + // label: "Legal", + // items: [ + // { + // href: "/terms", + // name: "Terms", + // }, + // { + // href: "/privacy", + // name: "Privacy", + // }, + // ], + // }, +]; + +const footerSocials = [ + { + href: "https://discord.com", + name: "Discord", + icon:+++ ++ ++ + Sign In
++ Join our community and unlock the full potential of Dingify. Sign in + effortlessly with Google to start managing your alerts. +
++ ++, + }, + { + href: "https://twitter.com", + name: "Twitter", + icon: , + }, +]; + +export function SiteFooter({ className }: React.HTMLAttributes ) { + return ( + + ); +} diff --git a/apps/www/src/components/layout/user-account-nav.tsx b/apps/www/src/components/layout/user-account-nav.tsx new file mode 100644 index 0000000..6478c7c --- /dev/null +++ b/apps/www/src/components/layout/user-account-nav.tsx @@ -0,0 +1,100 @@ +"use client"; + +import type { User } from "next-auth"; +import Link from "next/link"; +import { + Book, + CreditCard, + LayoutDashboard, + LogOut, + Settings, +} from "lucide-react"; +import { signOut } from "next-auth/react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; + +import { UserAvatar } from "@/components/shared/user-avatar"; + +interface UserAccountNavProps extends React.HTMLAttributes { + user: Pick ; +} + +export function UserAccountNav({ user }: UserAccountNavProps) { + return ( + + + ); +} diff --git a/apps/www/src/components/modal-provider.tsx b/apps/www/src/components/modal-provider.tsx new file mode 100644 index 0000000..e087e91 --- /dev/null +++ b/apps/www/src/components/modal-provider.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { SignInModal } from "@/components/layout/sign-in-modal"; +import { useMounted } from "@/hooks/use-mounted"; + +import { LanguageModal } from "./layout/language-modal"; + +export const ModalProvider = () => { + const mounted = useMounted(); + + if (!mounted) { + return null; + } + + return ( + <> ++ ++ + ++++ {user.name &&+{user.name}
} + {user.email && ( ++ {user.email} +
+ )} ++ + + ++ Dashboard
+ ++ + ++ Docs
+ ++ + ++ Billing
+ ++ + ++ Settings
+ ++ { + event.preventDefault(); + signOut({ + callbackUrl: `${window.location.origin}/`, + }); + }} + > + ++++ Log out
++ + {/* add your own modals here... */} + > + ); +}; diff --git a/apps/www/src/components/notifications/NotificationAlert.tsx b/apps/www/src/components/notifications/NotificationAlert.tsx new file mode 100644 index 0000000..016643a --- /dev/null +++ b/apps/www/src/components/notifications/NotificationAlert.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { updateNotificationSettings } from "@/actions/change-notification-settings"; +import { testWebhook } from "@/actions/testwebhook"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Check, ExternalLinkIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +import { Button } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@dingify/ui/components/form"; +import { Input } from "@dingify/ui/components/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@dingify/ui/components/select"; + +const FormSchema = z.object({ + name: z.string().min(2, { + message: "Name must be at least 2 characters.", + }), + provider: z.string({ + required_error: "Please select a provider.", + }), + webhook: z.string().min(2, { + message: "Webhook must be at least 2 characters.", + }), +}); + +export function NotificationAlert({ initialSettings }) { + const [isTesting, setIsTesting] = useState(false); // Manage testing state + const form = useForm >({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: initialSettings?.details?.name || "", + provider: initialSettings?.type || "", + webhook: initialSettings?.details?.webhook || "", + }, + }); + + const selectedProvider = form.watch("provider"); + + useEffect(() => { + form.reset({ + name: initialSettings?.details?.name || "", + provider: initialSettings?.type || "", + webhook: initialSettings?.details?.webhook || "", + }); + }, [initialSettings, form]); + + async function onSubmit(data: z.infer ) { + toast.promise(updateNotificationSettings(data), { + loading: "Updating...", + success: (result) => { + if (result.success) { + return "Notification Settings Updated."; + } else { + throw new Error(result.error); + } + }, + error: (err) => `Error updating notification settings: ${err.message}`, + }); + } + + async function handleTestWebhook() { + const data = form.getValues(); // Get current form values + setIsTesting(true); + + toast.promise( + testWebhook(data) + .then((result) => { + setIsTesting(false); + if (result.success) { + return "Webhook is working!"; + } else { + throw new Error(result.error); + } + }) + .catch((error) => { + setIsTesting(false); + throw new Error(`Error testing webhook: ${error.message}`); + }), + { + loading: "Testing webhook...", + success: "Webhook is working!", + error: (err) => `Error testing webhook: ${err.message}`, + }, + ); + } + + return ( + <> + + + > + ); +} diff --git a/apps/www/src/components/open-page/Open.tsx b/apps/www/src/components/open-page/Open.tsx new file mode 100644 index 0000000..48ff635 --- /dev/null +++ b/apps/www/src/components/open-page/Open.tsx @@ -0,0 +1,110 @@ +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +export default function OpenCardSection() { + return ( + + + ); +} diff --git a/apps/www/src/components/open-page/OpenCardFunding.tsx b/apps/www/src/components/open-page/OpenCardFunding.tsx new file mode 100644 index 0000000..9d4dec5 --- /dev/null +++ b/apps/www/src/components/open-page/OpenCardFunding.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +const data = [ + { name: "Jan", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Feb", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Mar", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Apr", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "May", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Jun", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Jul", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Aug", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Sep", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Oct", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Nov", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Dec", Total: Math.floor(Math.random() * 5000) + 1000 }, +]; + +// Custom Tooltip component +const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + return ( ++++ ++ +Total Revenue + ++ +$45,231.89++ +20.1% from last month +
++ ++ +Subscriptions + ++ ++2350++ +180.1% from last month +
++ ++ +Sales + ++ ++12,234++ +19% from last month +
++ ++ +Active Now + ++ ++573++ +201 since last hour +
+++ ); + } + + return null; +}; + +export default function OpenCardFunding() { + return ( +{`${payload[0].name} : ${payload[0].value} Users`}
++ + ); +} diff --git a/apps/www/src/components/open-page/OpenCardFundingChart.tsx b/apps/www/src/components/open-page/OpenCardFundingChart.tsx new file mode 100644 index 0000000..15d03bf --- /dev/null +++ b/apps/www/src/components/open-page/OpenCardFundingChart.tsx @@ -0,0 +1,71 @@ +"use client"; + +import React from "react"; +import { + Cell, + Legend, + Pie, + PieChart, + ResponsiveContainer, + Tooltip, +} from "recharts"; + +const data = [ + { name: "Founders", value: 90.0 }, + { name: "Team Pool", value: 10.0 }, +]; + +const COLORS = ["#888888", "#FAFAFA"]; // Adjust colors for better visibility if necessary + +// Custom Tooltip component +// @ts-ignore +const CustomTooltip = ({ active, payload }) => { + if (active && payload && payload.length) { + return ( ++ ++ `${value}`} + /> + + } /> + ++ ); + } + + return null; +}; + +// Custom legend formatter function +// @ts-ignore +const renderColorfulLegendText = (value, entry) => { + const { color } = entry; + return {`${value} ${entry.payload.value}%`}; +}; + +export default function OpenCardFundingChart() { + return ( +{`${payload[0].name} : ${payload[0].value} %`}
++ + ); +} diff --git a/apps/www/src/components/open-page/OpenCardFundingDiagram.tsx b/apps/www/src/components/open-page/OpenCardFundingDiagram.tsx new file mode 100644 index 0000000..6bae37b --- /dev/null +++ b/apps/www/src/components/open-page/OpenCardFundingDiagram.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +const data = [ + { name: "Jan", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Feb", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Mar", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Apr", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "May", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Jun", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Jul", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Aug", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Sep", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Oct", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Nov", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Dec", Total: Math.floor(Math.random() * 5000) + 1000 }, +]; + +// Custom Tooltip component +const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + return ( ++ ++ {data.map((entry, index) => ( + ++ ))} + | } + /> + + ++ ); + } + + return null; +}; + +export default function OpenCardFundingDiagram() { + return ( +{`${payload[0].name} : ${payload[0].value} Users`}
++ + ); +} diff --git a/apps/www/src/components/open-page/OpenCardNewUsers.tsx b/apps/www/src/components/open-page/OpenCardNewUsers.tsx new file mode 100644 index 0000000..3f688b8 --- /dev/null +++ b/apps/www/src/components/open-page/OpenCardNewUsers.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +const data = [ + { name: "Jan", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Feb", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Mar", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Apr", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "May", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Jun", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Jul", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Aug", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Sep", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Oct", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Nov", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Dec", Total: Math.floor(Math.random() * 5000) + 1000 }, +]; + +// Custom Tooltip component +const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + return ( ++ ++ `${value}`} + /> + + } /> + ++ ); + } + + return null; +}; + +export default function OpenCardNewUsers() { + return ( +{`${payload[0].name} : ${payload[0].value} Users`}
++ + ); +} diff --git a/apps/www/src/components/open-page/OpenCardUsers.tsx b/apps/www/src/components/open-page/OpenCardUsers.tsx new file mode 100644 index 0000000..e487b29 --- /dev/null +++ b/apps/www/src/components/open-page/OpenCardUsers.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +const data = [ + { name: "Jan", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Feb", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Mar", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Apr", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "May", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Jun", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Jul", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Aug", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Sep", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Oct", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Nov", Total: Math.floor(Math.random() * 5000) + 1000 }, + { name: "Dec", Total: Math.floor(Math.random() * 5000) + 1000 }, +]; + +// Custom Tooltip component +const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + return ( ++ ++ `${value}`} + /> + + } /> + ++ ); + } + + return null; +}; + +export default function OpenCardUsers() { + return ( +{`${payload[0].name} : ${payload[0].value} Users`}
++ + ); +} diff --git a/apps/www/src/components/open-page/OpenCardsSection.tsx b/apps/www/src/components/open-page/OpenCardsSection.tsx new file mode 100644 index 0000000..b4000f1 --- /dev/null +++ b/apps/www/src/components/open-page/OpenCardsSection.tsx @@ -0,0 +1,71 @@ +import { + ActivityIcon, + GitBranchIcon, + GitPullRequestIcon, + StarIcon, +} from "lucide-react"; + +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +// Static data - replace these with actual data fetched from GitHub if needed +const githubData = { + stars: 5269, + openIssues: 43, + mergedPRs: 366, + totalContributors: 43, +}; + +// @ts-ignore +export default function OpenCardSection({ githubData }) { + return ( ++ ++ `${value}`} + /> + + } /> + + + ); +} diff --git a/apps/www/src/components/open-page/OpenMiddleSection.tsx b/apps/www/src/components/open-page/OpenMiddleSection.tsx new file mode 100644 index 0000000..99b021c --- /dev/null +++ b/apps/www/src/components/open-page/OpenMiddleSection.tsx @@ -0,0 +1,20 @@ +import { Separator } from "@dingify/ui/components/separator"; + +export default function OpenMiddleSection() { + return ( ++++ ++ +Stars ++ + ++ {githubData.stargazers_count} +++ ++ +Open Issues ++ + +{githubData.open_issues}++ ++ ++ Merged PR's + ++ + +{githubData.total_count}++ ++ +Forks ++ + +{githubData.forks}++ + ); +} diff --git a/apps/www/src/components/open-page/OpenSalaryTable.tsx b/apps/www/src/components/open-page/OpenSalaryTable.tsx new file mode 100644 index 0000000..7157980 --- /dev/null +++ b/apps/www/src/components/open-page/OpenSalaryTable.tsx @@ -0,0 +1,194 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table"; +import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"; + +import { Button } from "@dingify/ui/components/button"; +import { Checkbox } from "@dingify/ui/components/checkbox"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; +import { Input } from "@dingify/ui/components/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@dingify/ui/components/table"; + +const data: SalaryBand[] = [ + { + id: "1", + title: "Software Engineer - Intern", + seniority: "Intern", + salary: "$30,000", + }, + { + id: "2", + title: "Software Engineer - I", + seniority: "Junior", + salary: "$60,000", + }, + { + id: "3", + title: "Software Engineer - II", + seniority: "Mid", + salary: "$80,000", + }, + { + id: "4", + title: "Software Engineer - III", + seniority: "Senior", + salary: "$100,000", + }, + { + id: "5", + title: "Software Engineer - IV", + seniority: "Principal", + salary: "$120,000", + }, + { id: "6", title: "Designer - III", seniority: "Senior", salary: "$100,000" }, + { + id: "7", + title: "Designer - IV", + seniority: "Principal", + salary: "$120,000", + }, + { id: "8", title: "Marketer - I", seniority: "Junior", salary: "$50,000" }, + { id: "9", title: "Marketer - II", seniority: "Mid", salary: "$65,000" }, + { id: "10", title: "Marketer - III", seniority: "Senior", salary: "$80,000" }, +]; + +export type SalaryBand = { + id: string; + title: string; + seniority: string; + salary: string; +}; + +export const columns: ColumnDef+ {/*++ Open startup +
*/} ++ Funding +
++ We dont have done any funding runs yet, should we? +
++ [] = [ + { + accessorKey: "title", + header: "Title", + cell: ({ row }) => {row.getValue("title")}, + }, + { + accessorKey: "seniority", + header: "Seniority", + cell: ({ row }) =>{row.getValue("seniority")}, + }, + { + accessorKey: "salary", + header: "Salary", + cell: ({ row }) =>{row.getValue("salary")}, + }, +]; + +export function OpenSalaryTable() { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState ( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState ({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( + + + ); +} diff --git a/apps/www/src/components/open-page/OpenStartupSection.tsx b/apps/www/src/components/open-page/OpenStartupSection.tsx new file mode 100644 index 0000000..b58e64d --- /dev/null +++ b/apps/www/src/components/open-page/OpenStartupSection.tsx @@ -0,0 +1,17 @@ +export default function OpenStartupSection() { + return ( ++++ Global Salary Bands +
++++
++ {table.getHeaderGroups().map((headerGroup) => ( + ++ {headerGroup.headers.map((header) => { + return ( + + ))} ++ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} ++ {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + ++ {row.getVisibleCells().map((cell) => ( + + )) + ) : ( ++ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} ++ + )} ++ No results. + ++ + ); +} diff --git a/apps/www/src/components/open-page/OpenTableTeam.tsx b/apps/www/src/components/open-page/OpenTableTeam.tsx new file mode 100644 index 0000000..afa48c2 --- /dev/null +++ b/apps/www/src/components/open-page/OpenTableTeam.tsx @@ -0,0 +1,190 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table"; +import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"; + +import { Button } from "@dingify/ui/components/button"; +import { Checkbox } from "@dingify/ui/components/checkbox"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; +import { Input } from "@dingify/ui/components/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@dingify/ui/components/table"; + +const data: TeamMember[] = [ + { + id: "1", + name: "Christer Hagen", + role: "Founder, CEO", + salary: "$0", + engagement: "Full-Time", + location: "Norway", + joinDate: "February 4th, 2024", + }, + // { + // id: "2", + // name: "Lucas Smith", + // role: "Co-Founder, CTO", + // salary: "$95,000", + // engagement: "Full-Time", + // location: "Australia", + // joinDate: "April 19th, 2023", + // }, +]; + +export type TeamMember = { + id: string; + name: string; + role: string; + salary: string; + engagement: string; + location: string; + joinDate: string; +}; + +export const columns: ColumnDef+++ Open startup +
++ Everything Open! +
++ We want to make everything open, so lets share everything we have. +
+[] = [ + { + accessorKey: "name", + header: "Name", + cell: ({ row }) => {row.getValue("name")}, + }, + { + accessorKey: "role", + header: "Role", + cell: ({ row }) =>{row.getValue("role")}, + }, + { + accessorKey: "salary", + header: "Salary", + cell: ({ row }) =>{row.getValue("salary")}, + }, + { + accessorKey: "engagement", + header: "Engagement", + cell: ({ row }) =>{row.getValue("engagement")}, + }, + { + accessorKey: "location", + header: "Location", + cell: ({ row }) =>{row.getValue("location")}, + }, + { + accessorKey: "joinDate", + header: "Join Date", + cell: ({ row }) =>{row.getValue("joinDate")}, + }, +]; + +export function OpenTableTeam() { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState ( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState ({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( + + + ); +} diff --git a/apps/www/src/components/open-page/OpenUsersDiagram.tsx b/apps/www/src/components/open-page/OpenUsersDiagram.tsx new file mode 100644 index 0000000..5d09415 --- /dev/null +++ b/apps/www/src/components/open-page/OpenUsersDiagram.tsx @@ -0,0 +1,62 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +import OpenCardNewUsers from "./OpenCardNewUsers"; +import OpenCardUsers from "./OpenCardUsers"; + +export default function OpenUsersDiagram() { + return ( ++++ Team +
++++
++ {table.getHeaderGroups().map((headerGroup) => ( + ++ {headerGroup.headers.map((header) => { + return ( + + ))} ++ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} ++ {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + ++ {row.getVisibleCells().map((cell) => ( + + )) + ) : ( ++ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} ++ + )} ++ No results. + ++ + ); +} + +// OR if we want it to be bigger +{ + /*+++++ ++ +All users +Users all time ++ ++ + ++ +New users +New users the last 30 days ++ ++ +*/ +} diff --git a/apps/www/src/components/open-page/OpenUsersFunding.tsx b/apps/www/src/components/open-page/OpenUsersFunding.tsx new file mode 100644 index 0000000..3de860c --- /dev/null +++ b/apps/www/src/components/open-page/OpenUsersFunding.tsx @@ -0,0 +1,62 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +import OpenCardFundingChart from "./OpenCardFundingChart"; +import OpenCardFundingDiagram from "./OpenCardFundingDiagram"; + +export default function OpenUsersFunding() { + return ( ++ ++ +All users ++ ++ + ++ +New users +New users the last 30 days ++ ++ + + ); +} + +// OR if we want it to be bigger +{ + /*+++++ ++ +Total Funding Raised +All time ++ ++ + ++ +Cap Table +Allocation ++ ++ +*/ +} diff --git a/apps/www/src/components/open-page/OpenUsersText.tsx b/apps/www/src/components/open-page/OpenUsersText.tsx new file mode 100644 index 0000000..0493b31 --- /dev/null +++ b/apps/www/src/components/open-page/OpenUsersText.tsx @@ -0,0 +1,19 @@ +import { Separator } from "@dingify/ui/components/separator"; + +export default function OpenUsersText() { + return ( ++ ++ +All users ++ ++ + ++ +New users +New users the last 30 days ++ ++ + + ); +} diff --git a/apps/www/src/components/pricing-cards.tsx b/apps/www/src/components/pricing-cards.tsx new file mode 100644 index 0000000..cd39508 --- /dev/null +++ b/apps/www/src/components/pricing-cards.tsx @@ -0,0 +1,158 @@ +"use client"; + +import type { UserSubscriptionPlan } from "@/types"; +import { Suspense, useState } from "react"; +import Link from "next/link"; +import { BillingFormButton } from "@/components/forms/billing-form-button"; +import { Icons } from "@/components/shared/icons"; +import { pricingData } from "@/config/subscriptions"; +import { useSigninModal } from "@/hooks/use-signin-modal"; +import Balancer from "react-wrap-balancer"; + +import { Button, buttonVariants } from "@dingify/ui/components/button"; +import { Switch } from "@dingify/ui/components/switch"; + +interface PricingCardsProps { + userId?: string; + subscriptionPlan?: UserSubscriptionPlan; +} + +export function PricingCards({ userId, subscriptionPlan }: PricingCardsProps) { + const isYearlyDefault = + !subscriptionPlan?.interval || subscriptionPlan.interval === "year" + ? true + : false; + const [isYearly, setIsYearly] = useState+ {/*++ Open startup +
*/} ++ Our users +
++ How is the growth? +
+(!!isYearlyDefault); + const signInModal = useSigninModal(); + + const toggleBilling = () => { + setIsYearly(!isYearly); + }; + + return ( + + + ); +} diff --git a/apps/www/src/components/pricing-faq.tsx b/apps/www/src/components/pricing-faq.tsx new file mode 100644 index 0000000..962f795 --- /dev/null +++ b/apps/www/src/components/pricing-faq.tsx @@ -0,0 +1,74 @@ +import Balancer from "react-wrap-balancer"; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@dingify/ui/components/accordion"; + +const pricingFaqData = [ + { + id: "item-1", + question: "How does Dingify help with real-time monitoring and analytics?", + answer: + "Dingify provides comprehensive real-time monitoring and analytics by capturing critical business events, tracking user journeys, and offering detailed KPIs and insights. Make data-driven decisions to optimize your business performance.", + }, + { + id: "item-2", + question: "Can I integrate Dingify with my existing business tools?", + answer: + "Absolutely! Dingify is built with integration in mind. Our comprehensive API allows you to seamlessly connect with your existing business tools, ensuring a smooth addition to your current workflow.", + }, + { + id: "item-3", + question: "What kind of support can I expect with Dingify?", + answer: + "We offer dedicated support for all our users. Whether you're on a free or paid plan, our team is ready to assist you with any questions or issues you may encounter. Premium support options are available for our Pro plan subscribers.", + }, + { + id: "item-4", + question: "Is my data secure with Dingify?", + answer: + "Data security is our top priority. We employ SSL encryption and adhere to industry best practices to ensure that all your data, from event tracking to customer information, is securely stored and protected.", + }, + { + id: "item-5", + question: "How does the free plan differ from the paid plans?", + answer: + "The free plan offers basic features that allow you to experience the advantages of Dingify's real-time monitoring. Our paid plans provide access to more advanced features, including detailed analytics, priority support, and increased event volume.", + }, + { + id: "item-6", + question: "What additional features does the Pro plan include?", + answer: + "The Pro plan includes everything in the Basic plan plus advanced analytics, priority support, higher event volume, custom API integrations, and access to new features before they're publicly released.", + }, +]; + +export function PricingFaq() { + return ( +++ ++ Pricing +
++ Start at full speed ! +
++ Monthly Billing ++ ++ Annual Billing + + {pricingData.map((offer) => ( ++ +++ ))} +++ ++ {offer.title} +
+ +++ {offer.prices.monthly > 0 ? ( ++++ {isYearly && offer.prices.monthly > 0 ? ( + <> + + ${offer.prices.monthly} + + ${offer.prices.yearly / 12} + > + ) : ( + `$${offer.prices.monthly}` + )} ++++/mo++ {isYearly + ? `$${offer.prices.yearly} will be charged when annual` + : "when charged monthly"} ++ ) : null} ++++ {offer.benefits.map((feature) => ( +
+ + {userId && subscriptionPlan ? ( + offer.title === "Starter" ? ( + + Go to dashboard + + ) : ( +- +
+ ))} + + {offer.limitations.length > 0 && + offer.limitations.map((feature) => ( ++ {feature}
+- +
+ ))} ++ {feature}
++ ) + ) : ( + + )} + +
++ Email{" "} + + support@propwrite.com + {" "} + for to contact our support team. + +
+ + You can test the subscriptions and won't be charged. + ++ + ); +} diff --git a/apps/www/src/components/properties/NoPhotoPlaceholder copy.tsx b/apps/www/src/components/properties/NoPhotoPlaceholder copy.tsx new file mode 100644 index 0000000..14e7ff7 --- /dev/null +++ b/apps/www/src/components/properties/NoPhotoPlaceholder copy.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Button } from "@dingify/ui/components/button"; + +import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; + +import { DocsButton } from "../buttons/DocsButton"; + +export default function NoPhotoPlaceholder() { + return ( ++++
+Frequently Asked Questions ++
++ Explore our comprehensive FAQ to find quick answers to common + inquiries. If you need further assistance, don't hesitate to + contact us for personalized help. + ++ {pricingFaqData.map((faqItem) => ( + ++ + ))} +{faqItem.question} +{faqItem.answer} ++ + ); +} diff --git a/apps/www/src/components/properties/NoSummaryPlaceholder.tsx b/apps/www/src/components/properties/NoSummaryPlaceholder.tsx new file mode 100644 index 0000000..ab63311 --- /dev/null +++ b/apps/www/src/components/properties/NoSummaryPlaceholder.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; + +import { Button } from "@dingify/ui/components/button"; + +export default function NoSummaryPlaceholder({ propertyId, slug }) { + return ( ++ Your events ++ Your events for this channel will appear here. + ++ + +++ + + ); +} diff --git a/apps/www/src/components/properties/NoTextPlaceholder.tsx b/apps/www/src/components/properties/NoTextPlaceholder.tsx new file mode 100644 index 0000000..27cfc80 --- /dev/null +++ b/apps/www/src/components/properties/NoTextPlaceholder.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; + +import GenerateDescriptionButton2 from "../buttons/GenerateDescriptionButton2"; + +export default function NoTextPlaceholder({ propertyId, setDescriptionData }) { + return ( ++ Upload pictures ++ Upload pictures to start and let us create the text + + ++ + ); +} diff --git a/apps/www/src/components/properties/Propertiestable.tsx b/apps/www/src/components/properties/Propertiestable.tsx new file mode 100644 index 0000000..387065e --- /dev/null +++ b/apps/www/src/components/properties/Propertiestable.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; + +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableRow, +} from "@dingify/ui/components/table"; + +const PropertiesTable = ({ properties }) => { + const [sortKey, setSortKey] = useState("createdAt"); // default sort key + const [sortOrder, setSortOrder] = useState("desc"); // default sort order + const [filter, setFilter] = useState(""); + + // Sort and filter the properties + const sortedFilteredProperties = properties + .filter((property) => + property.address.toLowerCase().includes(filter.toLowerCase()) + ) + .sort((a, b) => { + if (a[sortKey] < b[sortKey]) return sortOrder === "asc" ? -1 : 1; + if (a[sortKey] > b[sortKey]) return sortOrder === "asc" ? 1 : -1; + return 0; + }); + + const toggleSort = (key) => { + if (sortKey === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortKey(key); + setSortOrder("asc"); + } + }; + + return ( + <> + setFilter(e.target.value)} + className="mb-4 w-full rounded-md border-2 px-4 py-2" + /> ++ Generate description ++ Let Propwrite make the summary and text for you. + ++ {/* */} + +
+ > + ); +}; + +export default PropertiesTable; diff --git a/apps/www/src/components/properties/PropertyImageWithOptions.tsx b/apps/www/src/components/properties/PropertyImageWithOptions.tsx new file mode 100644 index 0000000..bc46c22 --- /dev/null +++ b/apps/www/src/components/properties/PropertyImageWithOptions.tsx @@ -0,0 +1,44 @@ +// components/properties/PropertyImageWithOptions.js +import { SelectInputForm } from "../forms/select-input-form"; +import PropertyPicture from "./PropertyPicture"; + +const PropertyImageWithOptions = ({ image }) => { + console.log(image); + // Transform the image options into the correct format for SelectInputForm + const options = [ + { + key: "option1", + label: "Option 1", + description: image.option1, + imageId: image.id, + selectedOption: image.selectedOption, + }, + { + key: "option2", + label: "Option 2", + description: image.option2, + imageId: image.id, + selectedOption: image.selectedOption, + }, + { + key: "option3", + label: "Option 3", + description: image.option3, + imageId: image.id, + selectedOption: image.selectedOption, + }, + ].filter((option) => option.description); // Filter out options without a description + + return ( +A list of your properties. + ++ + +toggleSort("address")}>Address +toggleSort("createdAt")}> + Date Added + ++ {sortedFilteredProperties.map((property) => ( + ++ + ))} ++ + {property.address} + + ++ {new Date(property.createdAt).toLocaleDateString()} + +++ ); +}; + +export default PropertyImageWithOptions; diff --git a/apps/www/src/components/properties/PropertyPicture.tsx b/apps/www/src/components/properties/PropertyPicture.tsx new file mode 100644 index 0000000..e5795c4 --- /dev/null +++ b/apps/www/src/components/properties/PropertyPicture.tsx @@ -0,0 +1,25 @@ +import Image from "next/image"; + +export default function PropertyPicture({ src, alt }) { + // If you want to have a default image, you can set it here + const defaultImage = "default_image_path.jpg"; + + return ( ++++ +++ ++ ); +} + +PropertyPicture.defaultProps = { + src: "https://images.finncdn.no/dynamic/1600w/2023/11/vertical-2/10/2/318/430/822_1071303782.jpg", // Default src as empty string or any default image path + alt: "Property Image", // Default alt text + width: 500, // Example default width + height: 300, // Example default height +}; diff --git a/apps/www/src/components/providers.tsx b/apps/www/src/components/providers.tsx new file mode 100644 index 0000000..f6763e7 --- /dev/null +++ b/apps/www/src/components/providers.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { ThemeProviderProps } from "next-themes/dist/types"; +import { Provider as BalancerProvider } from "react-wrap-balancer"; + +import { TooltipProvider } from "@dingify/ui/components/tooltip"; + +export function Providers({ children, ...props }: ThemeProviderProps) { + return ( ++ + + ); +} diff --git a/apps/www/src/components/shared/callout.tsx b/apps/www/src/components/shared/callout.tsx new file mode 100644 index 0000000..6655e09 --- /dev/null +++ b/apps/www/src/components/shared/callout.tsx @@ -0,0 +1,32 @@ +import { cn } from "@/lib/utils"; + +interface CalloutProps { + icon?: string; + children?: React.ReactNode; + type?: "default" | "warning" | "danger" | "info"; +} + +// โ ๐กโ ๏ธ๐ซ๐จ +export function Callout({ + children, + icon, + type = "default", + ...props +}: CalloutProps) { + return ( ++ ++ +{children} ++ {icon && {icon}} ++ ); +} diff --git a/apps/www/src/components/shared/card-skeleton.tsx b/apps/www/src/components/shared/card-skeleton.tsx new file mode 100644 index 0000000..c83ea38 --- /dev/null +++ b/apps/www/src/components/shared/card-skeleton.tsx @@ -0,0 +1,22 @@ +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from "@dingify/ui/components/card"; +import { Skeleton } from "@dingify/ui/components/skeleton"; + +export function CardSkeleton() { + return ( +{children}++ + ); +} diff --git a/apps/www/src/components/shared/empty-placeholder.tsx b/apps/www/src/components/shared/empty-placeholder.tsx new file mode 100644 index 0000000..a457a62 --- /dev/null +++ b/apps/www/src/components/shared/empty-placeholder.tsx @@ -0,0 +1,81 @@ +import * as React from "react"; +import { Icons } from "@/components/shared/icons"; +import { cn } from "@/lib/utils"; + +type EmptyPlaceholderProps = React.HTMLAttributes+ ++ + + + ++ ; + +export function EmptyPlaceholder({ + className, + children, + ...props +}: EmptyPlaceholderProps) { + return ( + ++ ); +} + +interface EmptyPlaceholderIconProps + extends Partial+ {children} ++> { + name: keyof typeof Icons; + ref?: + | ((instance: SVGSVGElement | null) => void) + | React.RefObject + | null; +} + +EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({ + name, + className, + ...props +}: EmptyPlaceholderIconProps) { + const Icon = Icons[name]; + + if (!Icon) { + return null; + } + + return ( + ++ ); +}; + +type EmptyPlacholderTitleProps = React.HTMLAttributes+ ; + +EmptyPlaceholder.Title = function EmptyPlaceholderTitle({ + className, + ...props +}: EmptyPlacholderTitleProps) { + return ( + + ); +}; + +type EmptyPlaceholderDescriptionProps = + React.HTMLAttributes ; + +EmptyPlaceholder.Description = function EmptyPlaceholderDescription({ + className, + ...props +}: EmptyPlaceholderDescriptionProps) { + return ( + + ); +}; diff --git a/apps/www/src/components/shared/icons.tsx b/apps/www/src/components/shared/icons.tsx new file mode 100644 index 0000000..83b2c40 --- /dev/null +++ b/apps/www/src/components/shared/icons.tsx @@ -0,0 +1,121 @@ +import { + AlertTriangle, + ArrowRight, + Bell, + BrainCircuit, + Check, + ChevronDown, + ChevronLeft, + ChevronRight, + CreditCard, + File, + FileText, + HelpCircle, + Home, + Image, + Laptop, + Loader2, + LucideIcon, + LucideProps, + Moon, + MoreVertical, + PartyPopper, + PieChart, + Plus, + Puzzle, + Rss, + Search, + Settings, + SunMedium, + Trash, + Ungroup, + User, + X, +} from "lucide-react"; + +export type Icon = LucideIcon; + +export const Icons = { + add: Plus, + arrowRight: ArrowRight, + billing: CreditCard, + chevronLeft: ChevronLeft, + chevronRight: ChevronRight, + check: Check, + close: X, + ellipsis: MoreVertical, + help: HelpCircle, + laptop: Laptop, + logo: Rss, + media: Image, + moon: Moon, + page: File, + post: FileText, + search: Search, + settings: Settings, + spinner: Loader2, + sun: SunMedium, + trash: Trash, + user: User, + warning: AlertTriangle, + home: Home, + piechart: PieChart, + chevrondown: ChevronDown, + brain: BrainCircuit, + ungroup: Ungroup, + party: PartyPopper, + piecart: PieChart, + bell: Bell, + gitHub: ({ ...props }: LucideProps) => ( + + ), + google: ({ ...props }: LucideProps) => ( + + ), + + twitter: ({ ...props }: LucideProps) => ( + + ), +}; diff --git a/apps/www/src/components/shared/modal.tsx b/apps/www/src/components/shared/modal.tsx new file mode 100644 index 0000000..3b83848 --- /dev/null +++ b/apps/www/src/components/shared/modal.tsx @@ -0,0 +1,52 @@ +"use client"; + +import useMediaQuery from "@/hooks/use-media-query"; +import { cn } from "@/lib/utils"; +import { Drawer } from "vaul"; + +import { Dialog, DialogContent } from "@dingify/ui/components/dialog"; + +interface ModalProps { + children: React.ReactNode; + className?: string; + showModal: boolean; + setShowModal: () => void; +} + +export function Modal({ + children, + className, + showModal, + setShowModal, +}: ModalProps) { + const { isMobile } = useMediaQuery(); + + if (isMobile) { + return ( + + + ); + } + return ( + + ); +} diff --git a/apps/www/src/components/shared/toc.tsx b/apps/www/src/components/shared/toc.tsx new file mode 100644 index 0000000..9f74e30 --- /dev/null +++ b/apps/www/src/components/shared/toc.tsx @@ -0,0 +1,113 @@ +"use client"; + +import type { TableOfContents } from "@/lib/toc"; +import * as React from "react"; +import { useMounted } from "@/hooks/use-mounted"; +import { cn } from "@/lib/utils"; + +interface TocProps { + toc: TableOfContents; +} + +export function DashboardTableOfContents({ toc }: TocProps) { + const itemIds = React.useMemo( + () => + toc.items + ? toc.items + .flatMap((item) => [item.url, item.items?.map((item) => item.url)]) + .flat() + .filter(Boolean) + .map((id) => id?.split("#")[1]) + : [], + [toc] + ); + const activeHeading = useActiveItem(itemIds); + const mounted = useMounted(); + + if (!toc.items) { + return null; + } + + return mounted ? ( ++ + ++ ++ ++ {children} ++ ++ ) : null; +} + +function useActiveItem(itemIds: (string | undefined)[]) { + const [activeId, setActiveId] = React.useStateOn This Page
++ (""); + + React.useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: `0% 0% -80% 0%` } + ); + + itemIds.forEach((id) => { + if (!id) { + return; + } + + const element = document.getElementById(id); + if (element) { + observer.observe(element); + } + }); + + return () => { + itemIds.forEach((id) => { + if (!id) { + return; + } + + const element = document.getElementById(id); + if (element) { + observer.unobserve(element); + } + }); + }; + }, [itemIds]); + + return activeId; +} + +interface TreeProps { + tree: TableOfContents; + level?: number; + activeItem?: string | null; +} + +function Tree({ tree, level = 1, activeItem }: TreeProps) { + return tree.items?.length && level < 3 ? ( + + {tree.items.map((item, index) => { + return ( +
+ ) : null; +} diff --git a/apps/www/src/components/shared/user-avatar.tsx b/apps/www/src/components/shared/user-avatar.tsx new file mode 100644 index 0000000..d4d5962 --- /dev/null +++ b/apps/www/src/components/shared/user-avatar.tsx @@ -0,0 +1,32 @@ +import type { User } from "@prisma/client"; +import type { AvatarProps } from "@radix-ui/react-avatar"; +import { Icons } from "@/components/shared/icons"; + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@dingify/ui/components/avatar"; + +interface UserAvatarProps extends AvatarProps { + user: Pick- + + {item.title} + + {item.items?.length ? ( +
+ ); + })} ++ ) : null} + ; +} + +export function UserAvatar({ user, ...props }: UserAvatarProps) { + return ( + + {user.image ? ( + + ); +} diff --git a/apps/www/src/components/table/dashboard/columns.tsx b/apps/www/src/components/table/dashboard/columns.tsx new file mode 100644 index 0000000..c6146a3 --- /dev/null +++ b/apps/www/src/components/table/dashboard/columns.tsx @@ -0,0 +1,120 @@ +"use client"; + +import Link from "next/link"; +import { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; + +import { Badge } from "@dingify/ui/components/badge"; +import { Button } from "@dingify/ui/components/button"; +import { Checkbox } from "@dingify/ui/components/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; + +import { propertyLabels, propertyStatuses } from "./propertystatus"; + +export type Payment = { + id: string; + amount: number; + status: "pending" | "processing" | "success" | "failed"; + email: string; +}; + +export type Property = { + id: string; + address: string; + status: "NOT_STARTED" | "IN_PROGRESS" | "DONE" | "CANCELED"; + label: "APARTMENT" | "HOUSE" | "CABIN" | "PROPERTY"; +}; + +export const columns: ColumnDef+ ) : ( + + {user.name} + + )} ++ [] = [ + { + accessorKey: "address", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const propertyLabel = propertyLabels.find( + (label) => label.value === row.original.label + ); + + return ( + ++ ); + }, + }, + { + accessorKey: "status", + header: () =>{propertyLabel?.label} + + {row.original.address} + +Status, + cell: ({ row }) => { + const status = propertyStatuses.find( + (status) => status.value === row.getValue("status") + ); + + if (!status) { + return null; + } + + return ( ++ {status.icon && ( ++ ); + }, + }, + { + id: "actions", + cell: ({ row }) => { + const payment = row.original; + + return ( ++ )} + {status.label} + ++ ); + }, + }, +]; diff --git a/apps/www/src/components/table/dashboard/data-table-faceted-filter.tsx b/apps/www/src/components/table/dashboard/data-table-faceted-filter.tsx new file mode 100644 index 0000000..6493517 --- /dev/null +++ b/apps/www/src/components/table/dashboard/data-table-faceted-filter.tsx @@ -0,0 +1,129 @@ +import type { Column } from "@tanstack/react-table"; +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { CheckIcon, PlusCircledIcon } from "@radix-ui/react-icons"; + +import { Badge } from "@dingify/ui/components/badge"; +import { Button } from "@dingify/ui/components/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@dingify/ui/components/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@dingify/ui/components/popover"; +import { Separator } from "@dingify/ui/components/separator"; + +interface DataTableFacetedFilter+ ++ + ++ +Actions +Edit +navigator.clipboard.writeText(payment.id)} + > + Copy payment ID + ++ View customer ++ + Delete + +โโซ +{ + column?: Column ; + title?: string; + options: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + }[]; +} + +export function DataTableFacetedFilter ({ + column, + title, + options, +}: DataTableFacetedFilter ) { + const facets = column?.getFacetedUniqueValues(); + const selectedValues = new Set(column?.getFilterValue() as string[]); + + return ( + + + ); +} diff --git a/apps/www/src/components/table/dashboard/data-table-toolbar.tsx b/apps/www/src/components/table/dashboard/data-table-toolbar.tsx new file mode 100644 index 0000000..336bc92 --- /dev/null +++ b/apps/www/src/components/table/dashboard/data-table-toolbar.tsx @@ -0,0 +1,63 @@ +"use client"; + +import type { Table } from "@tanstack/react-table"; +import { Cross2Icon } from "@radix-ui/react-icons"; + +import { Button } from "@dingify/ui/components/button"; +import { Input } from "@dingify/ui/components/input"; + +import { DataTableFacetedFilter } from "./data-table-faceted-filter"; +import { DataTableViewOptions } from "./data-table-view-options"; +// import { priorities, statuses } from "../data/data" + +import { propertyLabels, propertyStatuses } from "./propertystatus"; + +interface DataTableToolbarProps+ + ++ ++ ++ + +No results found. ++ {options.map((option) => { + const isSelected = selectedValues.has(option.value); + return ( + + {selectedValues.size > 0 && ( + <> +{ + // ... onSelect logic + }} + > + + ); + })} ++ {isSelected &&+ {option.icon && + React.createElement(option.icon, { + className: "mr-2 h-4 w-4 text-muted-foreground", + })} + {option.label} + {/* ... other item logic */} +} + + + + > + )} +column?.setFilterValue(undefined)} + className="justify-center text-center" + > + Clear filters + +{ + table: Table ; +} + +export function DataTableToolbar ({ + table, +}: DataTableToolbarProps ) { + const isFiltered = table.getState().columnFilters.length > 0; + + return ( + ++ ); +} diff --git a/apps/www/src/components/table/dashboard/data-table-view-options.tsx b/apps/www/src/components/table/dashboard/data-table-view-options.tsx new file mode 100644 index 0000000..2b88eca --- /dev/null +++ b/apps/www/src/components/table/dashboard/data-table-view-options.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { MixerHorizontalIcon } from "@radix-ui/react-icons"; +import { Table } from "@tanstack/react-table"; + +import { Button } from "@dingify/ui/components/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@dingify/ui/components/dropdown-menu"; + +interface DataTableViewOptionsProps+ + table.getColumn("address")?.setFilterValue(event.target.value) + } + className="h-8 w-[150px] lg:w-[250px]" + /> + {/* {table.getColumn("status") && ( +++ )} + {table.getColumn("label") && ( // Filter for property labels + + )} */} + {isFiltered && ( + + )} + + { + table: Table ; +} + +export function DataTableViewOptions ({ + table, +}: DataTableViewOptionsProps ) { + return ( + + + ); +} diff --git a/apps/www/src/components/table/dashboard/data-table.tsx b/apps/www/src/components/table/dashboard/data-table.tsx new file mode 100644 index 0000000..a96d1bd --- /dev/null +++ b/apps/www/src/components/table/dashboard/data-table.tsx @@ -0,0 +1,145 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table"; + +import { Button } from "@dingify/ui/components/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; +import { Input } from "@dingify/ui/components/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@dingify/ui/components/table"; + +import { DataTableToolbar } from "./data-table-toolbar"; + +interface DataTableProps+ + ++ +Toggle columns ++ {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ); + })} +{ + columns: ColumnDef []; + data: TData[]; +} + +export function DataTable ({ + columns, + data, +}: DataTableProps ) { + const [sorting, setSorting] = React.useState ([]); + const [columnFilters, setColumnFilters] = React.useState ( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState ({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( + ++ ); +} diff --git a/apps/www/src/components/table/dashboard/propertystatus.tsx b/apps/www/src/components/table/dashboard/propertystatus.tsx new file mode 100644 index 0000000..3c63862 --- /dev/null +++ b/apps/www/src/components/table/dashboard/propertystatus.tsx @@ -0,0 +1,62 @@ +import { + CheckCircledIcon, + CircleIcon, + CrossCircledIcon, + PersonIcon, + QuestionMarkCircledIcon, + StopwatchIcon, +} from "@radix-ui/react-icons"; + +export const propertyStatuses = [ + { + value: "NOT_STARTED", + label: "Not Started", + icon: QuestionMarkCircledIcon, // Replace with your chosen icon + }, + { + value: "IN_PROGRESS", + label: "In Progress", + icon: StopwatchIcon, // Replace with your chosen icon + }, + { + value: "DONE", + label: "Done", + icon: CheckCircledIcon, // Replace with your chosen icon + }, + { + value: "CANCELED", + label: "Canceled", + icon: CrossCircledIcon, // Replace with your chosen icon + }, +]; + +// export const propertyLabels = { +// APARTMENT: { label: "Apartment" /* icon or other attributes */ }, +// HOUSE: { label: "House" /* icon or other attributes */ }, +// CABIN: { label: "Cabin" /* icon or other attributes */ }, +// PROPERTY: { label: "Awaiting Details" /* icon or other attributes */ }, +// }; + +export const propertyLabels = [ + { + label: "Apartment", + value: "APARTMENT", + // icon: ApartmentIcon, // Optional, if you have an icon to represent this label + }, + { + label: "House", + value: "HOUSE", + // icon: HouseIcon, // Optional + }, + { + label: "Cabin", + value: "CABIN", + // icon: CabinIcon, // Optional + }, + { + label: "Awaiting Details", + value: "PROPERTY", + // icon: PropertyIcon, // Optional + }, + // ... other labels if you have more +]; diff --git a/apps/www/src/components/tailwind-indicator.tsx b/apps/www/src/components/tailwind-indicator.tsx new file mode 100644 index 0000000..20cbaf9 --- /dev/null +++ b/apps/www/src/components/tailwind-indicator.tsx @@ -0,0 +1,14 @@ +export function TailwindIndicator() { + if (process.env.NODE_ENV === "production") return null; + + return ( ++ +++
++ {table.getHeaderGroups().map((headerGroup) => ( + ++ {headerGroup.headers.map((header) => { + return ( + + ))} ++ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} ++ {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + ++ {row.getVisibleCells().map((cell) => ( + + )) + ) : ( ++ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} ++ + )} ++ No results. + ++ + ++++ ); +} diff --git a/apps/www/src/components/ui/animated-beam-multiple-outputs.tsx b/apps/www/src/components/ui/animated-beam-multiple-outputs.tsx new file mode 100644 index 0000000..60f3dea --- /dev/null +++ b/apps/www/src/components/ui/animated-beam-multiple-outputs.tsx @@ -0,0 +1,479 @@ +"use client"; + +import React, { forwardRef, useRef } from "react"; +import { IconProps } from "@radix-ui/react-icons/dist/types"; + +import { cn } from "@/lib/utils"; + +import { AnimatedBeam } from "../animate-beam"; + +const Circle = forwardRef< + HTMLDivElement, + { className?: string; children?: React.ReactNode } +>(({ className, children }, ref) => { + return ( +xs+sm+md+lg+xl+2xl++ {children} ++ ); +}); + +export function AnimatedBeamMultipleOutputDemo({ + className, +}: { + className?: string; +}) { + const containerRef = useRef(null); + const div1Ref = useRef (null); + const div2Ref = useRef (null); + const div3Ref = useRef (null); + const div4Ref = useRef (null); + const div5Ref = useRef (null); + const div6Ref = useRef (null); + const div7Ref = useRef (null); + + return ( + ++ ); +} + +const Icons = { + openai: (props: IconProps) => ( + + ), + user: (props: IconProps) => ( + + ), + googleDrive: (props: IconProps) => ( + + ), + googleDocs: (props: IconProps) => ( + + ), + whatsapp: (props: IconProps) => ( + + ), + messenger: (props: IconProps) => ( + + ), + notion: (props: IconProps) => ( + + ), +}; diff --git a/apps/www/src/components/ui/animated-beam.tsx b/apps/www/src/components/ui/animated-beam.tsx new file mode 100644 index 0000000..2f64c65 --- /dev/null +++ b/apps/www/src/components/ui/animated-beam.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { RefObject, useEffect, useId, useState } from "react"; +import { motion } from "framer-motion"; + +import { cn } from "@/lib/utils"; + +export interface AnimatedBeamProps { + className?: string; + containerRef: RefObject++ + {/* AnimatedBeams */} ++++ ++ +++ ++ +++ ++ + ++ + ++ + ++ + ++ + + + + + + ; // Container ref + fromRef: RefObject ; + toRef: RefObject ; + curvature?: number; + reverse?: boolean; + pathColor?: string; + pathWidth?: number; + pathOpacity?: number; + gradientStartColor?: string; + gradientStopColor?: string; + delay?: number; + duration?: number; + startXOffset?: number; + startYOffset?: number; + endXOffset?: number; + endYOffset?: number; +} + +export const AnimatedBeam: React.FC = ({ + className, + containerRef, + fromRef, + toRef, + curvature = 0, + reverse = false, // Include the reverse prop + duration = Math.random() * 3 + 4, + delay = 0, + pathColor = "gray", + pathWidth = 2, + pathOpacity = 0.2, + gradientStartColor = "#ffaa40", + gradientStopColor = "#9c40ff", + startXOffset = 0, + startYOffset = 0, + endXOffset = 0, + endYOffset = 0, +}) => { + const id = useId(); + const [pathD, setPathD] = useState(""); + const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 }); + + // Calculate the gradient coordinates based on the reverse prop + const gradientCoordinates = reverse + ? { + x1: ["90%", "-10%"], + x2: ["100%", "0%"], + y1: ["0%", "0%"], + y2: ["0%", "0%"], + } + : { + x1: ["10%", "110%"], + x2: ["0%", "100%"], + y1: ["0%", "0%"], + y2: ["0%", "0%"], + }; + + useEffect(() => { + const updatePath = () => { + if (containerRef.current && fromRef.current && toRef.current) { + const containerRect = containerRef.current.getBoundingClientRect(); + const rectA = fromRef.current.getBoundingClientRect(); + const rectB = toRef.current.getBoundingClientRect(); + + const svgWidth = containerRect.width; + const svgHeight = containerRect.height; + setSvgDimensions({ width: svgWidth, height: svgHeight }); + + const startX = + rectA.left - containerRect.left + rectA.width / 2 + startXOffset; + const startY = + rectA.top - containerRect.top + rectA.height / 2 + startYOffset; + const endX = + rectB.left - containerRect.left + rectB.width / 2 + endXOffset; + const endY = + rectB.top - containerRect.top + rectB.height / 2 + endYOffset; + + const controlY = startY - curvature; + const d = `M ${startX},${startY} Q ${ + (startX + endX) / 2 + },${controlY} ${endX},${endY}`; + setPathD(d); + } + }; + + // Initialize ResizeObserver + const resizeObserver = new ResizeObserver((entries) => { + // For all entries, recalculate the path + for (let entry of entries) { + updatePath(); + } + }); + + // Observe the container element + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + // Call the updatePath initially to set the initial path + updatePath(); + + // Clean up the observer on component unmount + return () => { + resizeObserver.disconnect(); + }; + }, [ + containerRef, + fromRef, + toRef, + curvature, + startXOffset, + startYOffset, + endXOffset, + endYOffset, + ]); + + return ( + + ); +}; diff --git a/apps/www/src/components/ui/animated-list-landing.tsx b/apps/www/src/components/ui/animated-list-landing.tsx new file mode 100644 index 0000000..34f2005 --- /dev/null +++ b/apps/www/src/components/ui/animated-list-landing.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +import { AnimatedList } from "./animated-list"; + +interface Item { + name: string; + description: string; + icon: string; + color: string; + time: string; +} + +let notifications = [ + { + name: "Payment received", + description: "User-123", + time: "15m ago", + icon: "๐ธ", + color: "#00C9A7", + }, + { + name: "User signed up", + description: "User-867", + time: "10m ago", + icon: "๐ค", + color: "#FFB800", + }, + { + name: "New message", + description: "User-456", + time: "5m ago", + icon: "๐ฌ", + color: "#FF3D71", + }, + { + name: "New event", + description: "User-789", + time: "2m ago", + icon: "๐๏ธ", + color: "#1E86FF", + }, +]; + +notifications = Array.from({ length: 10 }, () => notifications).flat(); + +const Notification = ({ name, description, icon, color, time }: Item) => { + return ( + + ); +}; + +export function AnimatedListLanding({ className }: { className?: string }) { + return ( + ++ ); +} diff --git a/apps/www/src/components/ui/animated-list.tsx b/apps/www/src/components/ui/animated-list.tsx new file mode 100644 index 0000000..d9928c9 --- /dev/null +++ b/apps/www/src/components/ui/animated-list.tsx @@ -0,0 +1,61 @@ +"use client"; + +import React, { ReactElement, useEffect, useMemo, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +export const AnimatedList = React.memo( + ({ + className, + children, + delay = 1000, + }: { + className?: string; + children: React.ReactNode; + delay?: number; + }) => { + const [index, setIndex] = useState(0); + const childrenArray = React.Children.toArray(children); + + useEffect(() => { + const interval = setInterval(() => { + setIndex((prevIndex) => (prevIndex + 1) % childrenArray.length); + }, delay); + + return () => clearInterval(interval); + }, [childrenArray.length, delay]); + + const itemsToShow = useMemo( + () => childrenArray.slice(0, index + 1).reverse(), + [index, childrenArray], + ); + + return ( ++ {notifications.map((item, idx) => ( + ++ ))} + ++ ); + }, +); + +AnimatedList.displayName = "AnimatedList"; + +export function AnimatedListItem({ children }: { children: React.ReactNode }) { + const animations = { + initial: { scale: 0, opacity: 0 }, + animate: { scale: 1, opacity: 1, originY: 0 }, + exit: { scale: 0, opacity: 0 }, + transition: { type: "spring", stiffness: 350, damping: 40 }, + }; + + return ( ++ {itemsToShow.map((item) => ( + ++ {item} + + ))} ++ {children} + + ); +} diff --git a/apps/www/src/components/ui/bento-grid.tsx b/apps/www/src/components/ui/bento-grid.tsx new file mode 100644 index 0000000..5ec43f1 --- /dev/null +++ b/apps/www/src/components/ui/bento-grid.tsx @@ -0,0 +1,80 @@ +import { ReactNode } from "react"; +import { ArrowRightIcon } from "@radix-ui/react-icons"; + +import { Button } from "@dingify/ui/components/button"; + +import { cn } from "@/lib/utils"; + +const BentoGrid = ({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) => { + return ( ++ {children} ++ ); +}; + +const BentoCard = ({ + name, + className, + background, + Icon, + description, + href, + cta, +}: { + name: string; + className: string; + background: ReactNode; + Icon: any; + description: string; + href: string; + cta: string; +}) => ( +++); + +export { BentoCard, BentoGrid }; diff --git a/apps/www/src/components/ui/bento-section-landing.tsx b/apps/www/src/components/ui/bento-section-landing.tsx new file mode 100644 index 0000000..69b8b63 --- /dev/null +++ b/apps/www/src/components/ui/bento-section-landing.tsx @@ -0,0 +1,143 @@ +import { CalendarIcon, FileTextIcon, InputIcon } from "@radix-ui/react-icons"; +import { BellIcon, MapIcon, Share2Icon } from "lucide-react"; + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@dingify/ui/components/command"; + +import { cn } from "@/lib/utils"; + +import { AnimatedBeamMultipleOutputDemo } from "./animated-beam-multiple-outputs"; +import { AnimatedListLanding } from "./animated-list-landing"; +import { BentoCard, BentoGrid } from "./bento-grid"; +import Marquee from "./marquee"; + +const files = [ + { + name: "First seen", + body: "The first time a user was seen in the app. This is the first event that was recorded for the user.", + }, + { + name: "Most used event", + body: "The event that the user has triggered the most. This is the event that the user has triggered the most.", + }, + { + name: "Last Seen", + body: "The last time a user was seen in the app. This is the last event that was recorded for the user.", + }, + { + name: "Events over time", + body: "The number of events that the user has triggered over time. This is the number of events that the user has triggered over time.", + }, + { + name: "Customer status", + body: "The status of the customer. How many time has the customer used your application", + }, +]; + +interface Item { + name: string; + description: string; + icon: string; + color: string; + time: string; +} + +const features = [ + { + Icon: MapIcon, + name: "See users journey", + description: "Understand how users interact with your app.", + href: "/", + cta: "Learn more", + className: "col-span-3 lg:col-span-1", + background: ( + + ), + }, + { + Icon: InputIcon, + name: "Get the information you need", + description: "Search through all your events fast", + href: "/", + cta: "Learn more", + className: "col-span-3 lg:col-span-2", + background: ( +{background}+++ ++ + {name} +
+{description}
++ ++ ++ + ), + }, + { + Icon: Share2Icon, + name: "Integrations", + description: "Supports 100+ integrations and counting.", + href: "/", + cta: "Learn more", + className: "col-span-3 lg:col-span-2", + background: ( ++ + +No results found. ++ +User-123 +Most used events +Analytics +All events +keys.gpg +seed.txt ++ ), + }, + { + Icon: BellIcon, + name: "Notifications", + description: "Get notified when somone uses your SDK", + className: "col-span-3 lg:col-span-1", + href: "/", + cta: "Learn more", + background: ( + + ), + }, +]; + +export function BentoSectionLanding() { + return ( + + {features.map((feature, idx) => ( + + ); +} diff --git a/apps/www/src/components/ui/border-beam.tsx b/apps/www/src/components/ui/border-beam.tsx new file mode 100644 index 0000000..a151795 --- /dev/null +++ b/apps/www/src/components/ui/border-beam.tsx @@ -0,0 +1,49 @@ +import { cn } from "@/lib/utils"; + +interface BorderBeamProps { + className?: string; + size?: number; + duration?: number; + borderWidth?: number; + anchor?: number; + colorFrom?: string; + colorTo?: string; + delay?: number; +} + +export const BorderBeam = ({ + className, + size = 100, + duration = 15, + anchor = 90, + borderWidth = 1.5, + colorFrom = "#ffaa40", + colorTo = "#9c40ff", + delay = 0, +}: BorderBeamProps) => { + return ( + + ); +}; diff --git a/apps/www/src/components/ui/confetti.tsx b/apps/www/src/components/ui/confetti.tsx new file mode 100644 index 0000000..11e6012 --- /dev/null +++ b/apps/www/src/components/ui/confetti.tsx @@ -0,0 +1,52 @@ +import confetti from "canvas-confetti"; + +interface ConfettiOptions extends confetti.Options { + particleCount?: number; + angle?: number; + spread?: number; + startVelocity?: number; + decay?: number; + gravity?: number; + drift?: number; + flat?: boolean; + ticks?: number; + origin?: { x: number; y: number }; + colors?: string[]; + shapes?: confetti.Shape[]; + zIndex?: number; + disableForReducedMotion?: boolean; + useWorker?: boolean; + resize?: boolean; + canvas?: HTMLCanvasElement | null; + scalar?: number; +} + +const Confetti = (options: ConfettiOptions) => { + if ( + options.disableForReducedMotion && + window.matchMedia("(prefers-reduced-motion)").matches + ) { + return; + } + + const confettiInstance = options.canvas + ? confetti.create(options.canvas, { + resize: options.resize ?? true, + useWorker: options.useWorker ?? true, + }) + : confetti; + + confettiInstance({ + ...options, + }); +}; + +Confetti.shapeFromPath = (options: { path: string; [key: string]: any }) => { + return confetti.shapeFromPath({ ...options }); +}; + +Confetti.shapeFromText = (options: { text: string; [key: string]: any }) => { + return confetti.shapeFromText({ ...options }); +}; + +export { Confetti }; diff --git a/apps/www/src/components/ui/feature-card.tsx b/apps/www/src/components/ui/feature-card.tsx new file mode 100644 index 0000000..37d5b94 --- /dev/null +++ b/apps/www/src/components/ui/feature-card.tsx @@ -0,0 +1,48 @@ +import { ReactNode } from "react"; + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@dingify/ui/components/command"; + +import { cn } from "@/lib/utils"; + +interface FeatureCardProps { + Icon: ReactNode; + name: string; + description: string; + href: string; + cta: string; + background: ReactNode; +} + +export function FeatureCard({ + Icon, + name, + description, + href, + cta, + background, +}: FeatureCardProps) { + return ( + + ); +} diff --git a/apps/www/src/components/ui/marquee.tsx b/apps/www/src/components/ui/marquee.tsx new file mode 100644 index 0000000..5605895 --- /dev/null +++ b/apps/www/src/components/ui/marquee.tsx @@ -0,0 +1,51 @@ +import { cn } from "@/lib/utils"; + +interface MarqueeProps { + className?: string; + reverse?: boolean; + pauseOnHover?: boolean; + children?: React.ReactNode; + vertical?: boolean; + repeat?: number; + [key: string]: any; +} + +export default function Marquee({ + className, + reverse, + pauseOnHover = false, + children, + vertical = false, + repeat = 4, + ...props +}: MarqueeProps) { + return ( ++ ))} + + {Array(repeat) + .fill(0) + .map((_, i) => ( ++ ); +} diff --git a/apps/www/src/components/ui/particles.tsx b/apps/www/src/components/ui/particles.tsx new file mode 100644 index 0000000..3465f95 --- /dev/null +++ b/apps/www/src/components/ui/particles.tsx @@ -0,0 +1,270 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; + +interface MousePosition { + x: number; + y: number; +} + +function MousePosition(): MousePosition { + const [mousePosition, setMousePosition] = useState+ {children} ++ ))} +({ + x: 0, + y: 0, + }); + + useEffect(() => { + const handleMouseMove = (event: MouseEvent) => { + setMousePosition({ x: event.clientX, y: event.clientY }); + }; + + window.addEventListener("mousemove", handleMouseMove); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + }; + }, []); + + return mousePosition; +} + +interface ParticlesProps { + className?: string; + quantity?: number; + staticity?: number; + ease?: number; + size?: number; + refresh?: boolean; + color?: string; + vx?: number; + vy?: number; +} +function hexToRgb(hex: string): number[] { + hex = hex.replace("#", ""); + const hexInt = parseInt(hex, 16); + const red = (hexInt >> 16) & 255; + const green = (hexInt >> 8) & 255; + const blue = hexInt & 255; + return [red, green, blue]; +} + +const Particles: React.FC = ({ + className = "", + quantity = 100, + staticity = 50, + ease = 50, + size = 0.4, + refresh = false, + color = "#ffffff", + vx = 0, + vy = 0, +}) => { + const canvasRef = useRef (null); + const canvasContainerRef = useRef (null); + const context = useRef (null); + const circles = useRef ([]); + const mousePosition = MousePosition(); + const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); + const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1; + + useEffect(() => { + if (canvasRef.current) { + context.current = canvasRef.current.getContext("2d"); + } + initCanvas(); + animate(); + window.addEventListener("resize", initCanvas); + + return () => { + window.removeEventListener("resize", initCanvas); + }; + }, [color]); + + useEffect(() => { + onMouseMove(); + }, [mousePosition.x, mousePosition.y]); + + useEffect(() => { + initCanvas(); + }, [refresh]); + + const initCanvas = () => { + resizeCanvas(); + drawParticles(); + }; + + const onMouseMove = () => { + if (canvasRef.current) { + const rect = canvasRef.current.getBoundingClientRect(); + const { w, h } = canvasSize.current; + const x = mousePosition.x - rect.left - w / 2; + const y = mousePosition.y - rect.top - h / 2; + const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2; + if (inside) { + mouse.current.x = x; + mouse.current.y = y; + } + } + }; + + type Circle = { + x: number; + y: number; + translateX: number; + translateY: number; + size: number; + alpha: number; + targetAlpha: number; + dx: number; + dy: number; + magnetism: number; + }; + + const resizeCanvas = () => { + if (canvasContainerRef.current && canvasRef.current && context.current) { + circles.current.length = 0; + canvasSize.current.w = canvasContainerRef.current.offsetWidth; + canvasSize.current.h = canvasContainerRef.current.offsetHeight; + canvasRef.current.width = canvasSize.current.w * dpr; + canvasRef.current.height = canvasSize.current.h * dpr; + canvasRef.current.style.width = `${canvasSize.current.w}px`; + canvasRef.current.style.height = `${canvasSize.current.h}px`; + context.current.scale(dpr, dpr); + } + }; + + const circleParams = (): Circle => { + const x = Math.floor(Math.random() * canvasSize.current.w); + const y = Math.floor(Math.random() * canvasSize.current.h); + const translateX = 0; + const translateY = 0; + const pSize = Math.floor(Math.random() * 2) + size; + const alpha = 0; + const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); + const dx = (Math.random() - 0.5) * 0.1; + const dy = (Math.random() - 0.5) * 0.1; + const magnetism = 0.1 + Math.random() * 4; + return { + x, + y, + translateX, + translateY, + size: pSize, + alpha, + targetAlpha, + dx, + dy, + magnetism, + }; + }; + + const rgb = hexToRgb(color); + + const drawCircle = (circle: Circle, update = false) => { + if (context.current) { + const { x, y, translateX, translateY, size, alpha } = circle; + context.current.translate(translateX, translateY); + context.current.beginPath(); + context.current.arc(x, y, size, 0, 2 * Math.PI); + context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`; + context.current.fill(); + context.current.setTransform(dpr, 0, 0, dpr, 0, 0); + + if (!update) { + circles.current.push(circle); + } + } + }; + + const clearContext = () => { + if (context.current) { + context.current.clearRect( + 0, + 0, + canvasSize.current.w, + canvasSize.current.h, + ); + } + }; + + const drawParticles = () => { + clearContext(); + const particleCount = quantity; + for (let i = 0; i < particleCount; i++) { + const circle = circleParams(); + drawCircle(circle); + } + }; + + const remapValue = ( + value: number, + start1: number, + end1: number, + start2: number, + end2: number, + ): number => { + const remapped = + ((value - start1) * (end2 - start2)) / (end1 - start1) + start2; + return remapped > 0 ? remapped : 0; + }; + + const animate = () => { + clearContext(); + circles.current.forEach((circle: Circle, i: number) => { + // Handle the alpha value + const edge = [ + circle.x + circle.translateX - circle.size, // distance from left edge + canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge + circle.y + circle.translateY - circle.size, // distance from top edge + canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge + ]; + const closestEdge = edge.reduce((a, b) => Math.min(a, b)); + const remapClosestEdge = parseFloat( + remapValue(closestEdge, 0, 20, 0, 1).toFixed(2), + ); + if (remapClosestEdge > 1) { + circle.alpha += 0.02; + if (circle.alpha > circle.targetAlpha) { + circle.alpha = circle.targetAlpha; + } + } else { + circle.alpha = circle.targetAlpha * remapClosestEdge; + } + circle.x += circle.dx + vx; + circle.y += circle.dy + vy; + circle.translateX += + (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / + ease; + circle.translateY += + (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / + ease; + + drawCircle(circle, true); + + // circle gets out of the canvas + if ( + circle.x < -circle.size || + circle.x > canvasSize.current.w + circle.size || + circle.y < -circle.size || + circle.y > canvasSize.current.h + circle.size + ) { + // remove the circle from the array + circles.current.splice(i, 1); + // create a new circle + const newCircle = circleParams(); + drawCircle(newCircle); + // update the circle position + } + }); + window.requestAnimationFrame(animate); + }; + + return ( + + ); +}; + +export default Particles; diff --git a/apps/www/src/components/ui/sphere-mask.tsx b/apps/www/src/components/ui/sphere-mask.tsx new file mode 100644 index 0000000..42e324a --- /dev/null +++ b/apps/www/src/components/ui/sphere-mask.tsx @@ -0,0 +1,25 @@ +import { cn } from "@/lib/utils"; + +export const SphereMask = ({ reverse = false }: { reverse?: boolean }) => { + return ( + + ); +}; diff --git a/apps/www/src/components/ui/text-shimmer.tsx b/apps/www/src/components/ui/text-shimmer.tsx new file mode 100644 index 0000000..74f29b2 --- /dev/null +++ b/apps/www/src/components/ui/text-shimmer.tsx @@ -0,0 +1,39 @@ +import { cn } from "@/lib/utils"; +import { CSSProperties, FC, ReactNode } from "react"; + +interface TextShimmerProps { + children: ReactNode; + className?: string; + shimmerWidth?: number; +} + +const TextShimmer: FC= ({ + children, + className, + shimmerWidth = 100, +}) => { + return ( + + {children} +
+ ); +}; + +export default TextShimmer; diff --git a/apps/www/src/components/users/AllUsersCard.tsx b/apps/www/src/components/users/AllUsersCard.tsx new file mode 100644 index 0000000..406ba2e --- /dev/null +++ b/apps/www/src/components/users/AllUsersCard.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { deleteCustomer } from "@/actions/delete-customer"; +import { ChevronDownIcon, PlusIcon, StarIcon } from "@radix-ui/react-icons"; +import { format } from "date-fns"; +import { CircleIcon, Tag, TrashIcon } from "lucide-react"; +import { toast } from "sonner"; + +import { Badge } from "@dingify/ui/components/badge"; +import { Button } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; +import { Separator } from "@dingify/ui/components/separator"; + +import { EditCustomerSheet } from "./EditCustomerSheet"; + +export function AllUsersCards({ customerDetails }) { + const router = useRouter(); + + const handleDelete = async (customerId) => { + // Add logic to delete customer if needed + try { + deleteCustomer(customerId); + toast.success("The customer has been deleted successfully."); + router.refresh(); + } catch (error) { + toast.error("There was an error deleting the customer."); + console.error("Error deleting customer:", error); + } + }; + + const handleEdit = (customerId) => { + // Display a toast message on edit click + toast.info(`Edit customer with ID: ${customerId}`); + }; + + const handleRedirect = (customerId) => { + router.push(`/dashboard/users/${customerId}`); + }; + + return ( ++ {customerDetails.map((customer) => ( ++ ); +} diff --git a/apps/www/src/components/users/DeleteDialogCustomer.tsx b/apps/www/src/components/users/DeleteDialogCustomer.tsx new file mode 100644 index 0000000..562859c --- /dev/null +++ b/apps/www/src/components/users/DeleteDialogCustomer.tsx @@ -0,0 +1,60 @@ +// components/DeleteCustomerDialog.tsx +"use client"; + +import { useRouter } from "next/navigation"; +import { deleteCustomer } from "@/actions/delete-customer"; +import { toast } from "sonner"; + +import { Button } from "@dingify/ui/components/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@dingify/ui/components/dialog"; + +export function DeleteCustomerDialog({ customerId }) { + const router = useRouter(); + + const handleDelete = async () => { + try { + const result = await deleteCustomer(customerId); + if (result.success) { + router.refresh(); + } else { + throw new Error(result.error); + } + } catch (error) { + toast.error("There was an error deleting the customer."); + console.error("Error deleting customer:", error); + } + }; + + return ( + + ); +} diff --git a/apps/www/src/components/users/EditCustomerSheet.tsx b/apps/www/src/components/users/EditCustomerSheet.tsx new file mode 100644 index 0000000..8059fc1 --- /dev/null +++ b/apps/www/src/components/users/EditCustomerSheet.tsx @@ -0,0 +1,119 @@ +// components/sheets/EditCustomerSheet.tsx +"use client"; + +import { useEffect } from "react"; +import { changeCustomerDetails } from "@/actions/change-customer-details"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Pencil, StarIcon, User, UserCog } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +import { Button } from "@dingify/ui/components/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@dingify/ui/components/form"; +import { Input } from "@dingify/ui/components/input"; +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@dingify/ui/components/sheet"; + +const FormSchema = z.object({ + name: z.string().min(1, "Name is required"), + email: z.string().email("Invalid email address"), +}); + +export function EditCustomerSheet({ customer }) { + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: customer.name || "", + email: customer.email || "", + }, + }); + + const onSubmit = async (data) => { + try { + const result = await changeCustomerDetails(customer.id, data); + if (result.success) { + toast.message( ++ + ))} ++ ++++ handleRedirect(customer.id)} + > + {customer.name || "Unnamed Customer"} + {" "} + {customer.icon} + +{customer.email || "No email"} ++++ + + ++ + ++ +Actions ++ handleDelete(customer.id)}> + ++ Delete + + + +Placeholder + + ++++++ {customer.userId} + +++ Placeholder + + {format(new Date(customer.createdAt), "dd.MM.yyyy HH:mm")} +++ Customer Updated. +, + ); + form.reset(); + } + } catch (error) { + toast.error("There was an error updating the customer."); + console.error("Error updating customer:", error); + } + }; + + return ( ++++ {JSON.stringify(data, null, 2)} +
++ + ); +} diff --git a/apps/www/src/components/users/EmailButton.tsx b/apps/www/src/components/users/EmailButton.tsx new file mode 100644 index 0000000..11f8048 --- /dev/null +++ b/apps/www/src/components/users/EmailButton.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Button } from "@dingify/ui/components/button"; + +interface EmailButtonProps { + email: string; + subject: string; + body: string; +} + +const EmailButton: React.FC+ + ++ ++ + + +Edit Customer +Update the customer's details. += ({ email, subject, body }) => { + const handleClick = () => { + const mailtoLink = `mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + window.location.href = mailtoLink; + }; + + return ( + + ); +}; + +export default EmailButton; diff --git a/apps/www/src/components/users/UserCard.tsx b/apps/www/src/components/users/UserCard.tsx new file mode 100644 index 0000000..c1bc71f --- /dev/null +++ b/apps/www/src/components/users/UserCard.tsx @@ -0,0 +1,11 @@ +import UserCardsSection from "./UserCardsSection"; +import { UserMainSection } from "./UserMainSection"; + +export default function UserCard({ customerDetails }) { + return ( + ++ ); +} diff --git a/apps/www/src/components/users/UserCardsSection.tsx b/apps/www/src/components/users/UserCardsSection.tsx new file mode 100644 index 0000000..0d295a9 --- /dev/null +++ b/apps/www/src/components/users/UserCardsSection.tsx @@ -0,0 +1,124 @@ +import { format, formatDistanceToNow } from "date-fns"; + +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +export default function UserCardsSection({ customerDetails }) { + return ( ++ + ++ ); +} diff --git a/apps/www/src/components/users/UserChartActivity.tsx b/apps/www/src/components/users/UserChartActivity.tsx new file mode 100644 index 0000000..0ab495e --- /dev/null +++ b/apps/www/src/components/users/UserChartActivity.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, +} from "recharts"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +const lineChartData = [ + { month: "Jan", events: 400, users: 240 }, + { month: "Feb", events: 300, users: 139 }, + { month: "Mar", events: 200, users: 980 }, + { month: "Apr", events: 278, users: 390 }, + { month: "May", events: 189, users: 480 }, + { month: "Jun", events: 239, users: 380 }, + { month: "Jul", events: 349, users: 430 }, + { month: "Aug", events: 430, users: 210 }, + { month: "Sep", events: 480, users: 340 }, + { month: "Oct", events: 390, users: 460 }, + { month: "Nov", events: 139, users: 220 }, + { month: "Dec", events: 240, users: 190 }, +]; + +export function UserChartActivity() { + return ( ++ ++ +First Seen + ++ ++ {" "} + {customerDetails.firstSeen + ? formatDistanceToNow(new Date(customerDetails.firstSeen), { + addSuffix: true, + }) + : "N/A"} +++ Since the user was made +
++ + ++ +Last Seen + ++ ++ {customerDetails.lastSeen + ? formatDistanceToNow(new Date(customerDetails.lastSeen), { + addSuffix: true, + }) + : "N/A"} ++Since last activity
++ ++ +Most Used Event + ++ ++ {customerDetails.mostUsedFeature || "N/A"} +++ Most frequently used event +
++ ++ +Total Events + ++ ++ {customerDetails.events ? customerDetails.events.length : "N/A"} ++Total events logged
++ + ); +} diff --git a/apps/www/src/components/users/UserEmailCard.tsx b/apps/www/src/components/users/UserEmailCard.tsx new file mode 100644 index 0000000..2c8d160 --- /dev/null +++ b/apps/www/src/components/users/UserEmailCard.tsx @@ -0,0 +1,44 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +import EmailButton from "./EmailButton"; // Adjust the import path as necessary + +export default function UserEmailCard({ customerDetails }) { + const email = customerDetails.email || "example@example.com"; + const name = customerDetails.name || "[Customer Name]"; + const subject = "Checking In: How's Your Experience with [Your Product]?"; + const body = ` + Hi ${name}, + + How is it going with [Your Product]? + + Just wanted to hear how the experience with [Your Product] has been. Would love to have a chat about how I can make the product better for you. + + Looking forward to hearing from you. + + Best regards, + [Your Name] + + [Your Contact Information] + `; + + return ( ++ +Events Over Time +Tracking the number of events month. ++ ++++ ++ +{ + if (active && payload && payload.length) { + return ( + ++ ); + } + + return null; + }} + /> ++ + {payload[0]?.payload.month} + + + Events: {payload[0]?.value} + + + Users: {payload[1]?.value} + +++ + + + ); +} diff --git a/apps/www/src/components/users/UserGridActivity.tsx b/apps/www/src/components/users/UserGridActivity.tsx new file mode 100644 index 0000000..50eef37 --- /dev/null +++ b/apps/www/src/components/users/UserGridActivity.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { + endOfMonth, + format, + parseISO, + startOfMonth, + subMonths, +} from "date-fns"; +import ActivityCalendar from "react-activity-calendar"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +type props = { + date: string; + count: number; + level: number; +}; + +// Example data: array of objects with date and count + +export function UserGridActivity({ dings }: { dings: any }) { + const today = new Date(); + + // Create a date 2 months back + last of current month - To fill in blanks + const firstDayTwoMonthsAgo = startOfMonth(subMonths(today, 2)); + const lastDayCurrentMonth = endOfMonth(today); + + const calendarData: any = [ + { date: format(firstDayTwoMonthsAgo, "yyyy-MM-dd"), count: 0, level: 0 }, + { date: format(lastDayCurrentMonth, "yyyy-MM-dd"), count: 0, level: 0 }, + ]; + dings.map((ding: any) => { + const eventData: props = { + date: format(ding.createdAt, "yyyy-MM-dd"), + count: 1, + level: 1, + }; + + // Check if date already exist in calendarData + const existingDate = calendarData.findIndex( + (event: any) => event.date === eventData.date, + ); + + // If date, update the event-count and level + if (existingDate !== -1) { + calendarData[existingDate].count = calendarData[existingDate].count + 1; + // Level 4 is max (dark green) + if (calendarData[existingDate].level < 4) + calendarData[existingDate].level = calendarData[existingDate].level + 1; + } else { + calendarData.push(eventData); + } + }); + + // Sort the calendarData by date - Prevents months from being hidden if no activity + calendarData.sort((a: any, b: any) => { + return parseISO(a.date).getTime() - parseISO(b.date).getTime(); + }); + + + return ( ++ +Follow up a customer ++ Stay in touch with your customers. Send them an email. + ++ + ++ + + ); +} diff --git a/apps/www/src/components/users/UserMainSection.tsx b/apps/www/src/components/users/UserMainSection.tsx new file mode 100644 index 0000000..ebb85ad --- /dev/null +++ b/apps/www/src/components/users/UserMainSection.tsx @@ -0,0 +1,88 @@ +import Image from "next/image"; +import Link from "next/link"; +import { + ChevronLeft, + Home, + LineChart, + Package, + Package2, + PanelLeft, + PlusCircle, + Search, + Settings, + ShoppingCart, + Upload, + Users2, +} from "lucide-react"; + +import { Badge } from "@dingify/ui/components/badge"; +import { Button } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; +import { Input } from "@dingify/ui/components/input"; +import { Label } from "@dingify/ui/components/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@dingify/ui/components/select"; +import { + Sheet, + SheetContent, + SheetTrigger, +} from "@dingify/ui/components/sheet"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@dingify/ui/components/table"; +import { Textarea } from "@dingify/ui/components/textarea"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@dingify/ui/components/tooltip"; + +import { UserChartActivity } from "./UserChartActivity"; +import UserEmailCard from "./UserEmailCard"; +import { UserGridActivity } from "./UserGridActivity"; +import { UserPowerCard } from "./UserPowerCard"; +import UsersDashboardTable from "./UsersDashboardTable"; + +export function UserMainSection({ customerDetails }) { + console.log(customerDetails) + return ( ++ +Activity Calendar ++ A visualization of user activity for the last 3 months. + ++ +++(activity) => { + alert(JSON.stringify(activity)); + }, + }} + /> + ++ ); +} diff --git a/apps/www/src/components/users/UserPowerCard.tsx b/apps/www/src/components/users/UserPowerCard.tsx new file mode 100644 index 0000000..ebedbba --- /dev/null +++ b/apps/www/src/components/users/UserPowerCard.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; + +export function UserPowerCard({ customerDetails }) { + return ( ++++++ + +++ + + + + ); +} diff --git a/apps/www/src/components/users/UsersCard.tsx b/apps/www/src/components/users/UsersCard.tsx new file mode 100644 index 0000000..92e3f38 --- /dev/null +++ b/apps/www/src/components/users/UsersCard.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { deleteEvent } from "@/actions/delete-event"; +import { + ChevronDownIcon, + CircleIcon, + PlusIcon, + StarIcon, +} from "@radix-ui/react-icons"; +import { format } from "date-fns"; +import { BellIcon, BellOffIcon, TrashIcon } from "lucide-react"; +import { toast } from "sonner"; + +import { Badge } from "@dingify/ui/components/badge"; +import { Button } from "@dingify/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@dingify/ui/components/dropdown-menu"; +import { Separator } from "@dingify/ui/components/separator"; + +export function UsersCard({ channelDetails }) { + const router = useRouter(); + + const handleDelete = async (eventId) => { + try { + await deleteEvent(eventId); + toast.success("The event has been deleted successfully."); + router.refresh(); + } catch (error) { + toast.error("There was an error deleting the event."); + console.error("Error deleting event:", error); + } + }; + + return ( ++ +Customer Status +Health of the customer ++ ++ {customerDetails.userStatus} +++ ++ ); +} diff --git a/apps/www/src/components/users/UsersDashboardTable.tsx b/apps/www/src/components/users/UsersDashboardTable.tsx new file mode 100644 index 0000000..755cc4c --- /dev/null +++ b/apps/www/src/components/users/UsersDashboardTable.tsx @@ -0,0 +1,57 @@ +import { format } from "date-fns"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@dingify/ui/components/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@dingify/ui/components/table"; + +// TODO Only show the 5 first events + +export default function UsersDashboardTable({ customerDetails }) { + return ( ++ + ); +} diff --git a/apps/www/src/config/dashboard.ts b/apps/www/src/config/dashboard.ts new file mode 100644 index 0000000..87c73c8 --- /dev/null +++ b/apps/www/src/config/dashboard.ts @@ -0,0 +1,59 @@ +import { DashboardConfig } from "@/types"; + +export const dashboardConfig: DashboardConfig = { + mainNav: [ + { + title: "Docs", + href: "https://docs.dingify.io/", + }, + { + title: "Support", + href: "/support", + disabled: true, + }, + ], + sidebarNav: [ + { + title: "Dashboard", + href: "/dashboard", + icon: "home", + }, + { + title: "Analytics", + href: "/dashboard/analytics", + icon: "piechart", + }, + { + title: "Users", + href: "/dashboard/users", + icon: "user", + }, + { + title: "Notifications", + href: "/dashboard/notifications", + icon: "bell", + }, + { + title: "Billing", + href: "/dashboard/billing", + icon: "billing", + }, + { + title: "Settings", + href: "/dashboard/settings", + icon: "settings", + }, + // { + // title: "Placeholder Names", + // href: "/dashboard/channels", + // }, + // { + // title: "Marketing", + // href: "/dashboard/channels/marketing", + // }, + // { + // title: "Sales", + // href: "/dashboard/channels/sales", + // }, + ], +}; diff --git a/apps/www/src/config/docs.ts b/apps/www/src/config/docs.ts new file mode 100644 index 0000000..785b702 --- /dev/null +++ b/apps/www/src/config/docs.ts @@ -0,0 +1,106 @@ +import { DocsConfig } from "@/types"; + +export const docsConfig: DocsConfig = { + mainNav: [ + { + title: "Documentation", + href: "/docs", + }, + { + title: "Guides", + href: "/guides", + }, + ], + sidebarNav: [ + { + title: "Getting Started", + items: [ + { + title: "Introduction", + href: "/docs", + }, + ], + }, + { + title: "Documentation", + items: [ + { + title: "Introduction", + href: "/docs/documentation", + }, + { + title: "Contentlayer", + href: "/docs/in-progress", + disabled: true, + }, + { + title: "Components", + href: "/docs/documentation/components", + }, + { + title: "Code Blocks", + href: "/docs/documentation/code-blocks", + }, + { + title: "Style Guide", + href: "/docs/documentation/style-guide", + }, + ], + }, + { + title: "Blog", + items: [ + { + title: "Introduction", + href: "/docs/in-progress", + disabled: true, + }, + { + title: "Build your own", + href: "/docs/in-progress", + disabled: true, + }, + { + title: "Writing Posts", + href: "/docs/in-progress", + disabled: true, + }, + ], + }, + { + title: "Dashboard", + items: [ + { + title: "Introduction", + href: "/docs/in-progress", + disabled: true, + }, + { + title: "Layouts", + href: "/docs/in-progress", + disabled: true, + }, + { + title: "Server Components", + href: "/docs/in-progress", + disabled: true, + }, + { + title: "Authentication", + href: "/docs/in-progress", + disabled: true, + }, + { + title: "Database with Prisma", + href: "/docs/in-progress", + disabled: true, + }, + { + title: "API Routes", + href: "/docs/in-progress", + disabled: true, + }, + ], + }, + ], +}; diff --git a/apps/www/src/config/marketing.ts b/apps/www/src/config/marketing.ts new file mode 100644 index 0000000..8d02c9b --- /dev/null +++ b/apps/www/src/config/marketing.ts @@ -0,0 +1,29 @@ +import type { MarketingConfig } from "@/types"; + +export const marketingConfig: MarketingConfig = { + mainNav: [ + { + title: "Docs", + href: "https://docs.dingify.io/", + }, + { + title: "Open Startup", + href: "/open", + }, + { + title: "Pricing", + href: "/pricing", + disabled: true, + }, + { + title: "Blog", + href: "/blog", + disabled: true, + }, + // { + // title: "Documentation", + // href: "/docs", + // disabled: true, + // }, + ], +}; diff --git a/apps/www/src/config/property.ts b/apps/www/src/config/property.ts new file mode 100644 index 0000000..9bb6657 --- /dev/null +++ b/apps/www/src/config/property.ts @@ -0,0 +1,41 @@ +import { PropertyConfig } from "@/types"; + +export const propertyConfig: PropertyConfig = { + mainNav: [ + { + title: "Dashboard", + href: "/dashboard", + }, + // { + // title: "Documentation", + // href: "/docs", + // }, + { + title: "Support", + href: "/support", + disabled: true, + }, + ], + sidebarNav: [ + { + title: "Summary", + href: "/", + icon: "piechart", + }, + { + title: "Pictures", + href: `/pictures`, + icon: "media", + }, + { + title: "Appraisal Report", + href: "/report", + icon: "home", + }, + // { + // title: "Settings", + // href: "/settings", + // icon: "settings", + // }, + ], +}; diff --git a/apps/www/src/config/site.ts b/apps/www/src/config/site.ts new file mode 100644 index 0000000..81fb241 --- /dev/null +++ b/apps/www/src/config/site.ts @@ -0,0 +1,17 @@ +import type { SiteConfig } from "@/types"; +import { env } from "@/env"; + +const site_url = env.NEXT_PUBLIC_APP_URL; + +export const siteConfig: SiteConfig = { + name: "Dingify", + description: + "Dingify revolutionizes alearts and notifications for developers and businesses", + url: site_url, + ogImage: `${site_url}/og.jpg`, + links: { + twitter: "https://twitter.com/codehagen", + github: "https://github.com/meglerhagen", + }, + mailSupport: "christer@sailsdock.com", +}; diff --git a/apps/www/src/config/subscriptions.ts b/apps/www/src/config/subscriptions.ts new file mode 100644 index 0000000..6c7a3fe --- /dev/null +++ b/apps/www/src/config/subscriptions.ts @@ -0,0 +1,71 @@ +import type { SubscriptionPlan } from "@/types"; +import { env } from "@/env"; + +export const pricingData: SubscriptionPlan[] = [ + { + title: "Starter", + description: "For Beginners", + benefits: [ + "Up to 3 listings", + "Basic analytics and reporting", + "Access to standard templates", + ], + limitations: [ + "No priority access to new features.", + "Limited customer support", + "No custom branding", + "Limited access to business resources.", + ], + prices: { + monthly: 0, + yearly: 0, + }, + stripeIds: { + monthly: null, + yearly: null, + }, + }, + { + title: "Pro", + description: "Unlock Advanced Features", + benefits: [ + "Up to 20 monthly listings", + "Advanced analytics and reporting", + "Access to business templates", + "Priority customer support", + "Exclusive webinars and training.", + ], + limitations: [ + "No custom branding", + "Limited access to business resources.", + ], + prices: { + monthly: 40, + yearly: 384, + }, + stripeIds: { + monthly: env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID, + yearly: env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID, + }, + }, + { + title: "Business", + description: "For Power Users", + benefits: [ + "Unlimited posts", + "Real-time analytics and reporting", + "Access to all templates, including custom branding", + "24/7 business customer support", + "Personalized onboarding and account management.", + ], + limitations: [], + prices: { + monthly: 60, + yearly: 576, + }, + stripeIds: { + monthly: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID, + yearly: env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID, + }, + }, +]; diff --git a/apps/www/src/content/authors/codehagen.mdx b/apps/www/src/content/authors/codehagen.mdx new file mode 100644 index 0000000..28fca2d --- /dev/null +++ b/apps/www/src/content/authors/codehagen.mdx @@ -0,0 +1,5 @@ +--- +title: Christer +avatar: /images/avatars/shadcn.png +twitter: codehagen +--- diff --git a/apps/www/src/content/authors/shadcn.mdx b/apps/www/src/content/authors/shadcn.mdx new file mode 100644 index 0000000..0883499 --- /dev/null +++ b/apps/www/src/content/authors/shadcn.mdx @@ -0,0 +1,5 @@ +--- +title: shadcn +avatar: /images/avatars/shadcn.png +twitter: shadcn +--- diff --git a/apps/www/src/content/blog/how-propwrite-makes-listings-irresistible.mdx b/apps/www/src/content/blog/how-propwrite-makes-listings-irresistible.mdx new file mode 100644 index 0000000..4444fbe --- /dev/null +++ b/apps/www/src/content/blog/how-propwrite-makes-listings-irresistible.mdx @@ -0,0 +1,41 @@ +--- +title: "AI in Real Estate: How Propwrite Makes Listings Irresistible" +description: Explore the transformative AI features of Propwrite that elevate real estate listings to new heights. +image: /images/blog/blog-post-3.jpg +date: "2023-11-23" +authors: + - codehagen +--- + ++ +Customer Journey ++ A detailed view of the customer's journey + ++ ++
++ +Timestamp +Event +Channel +Icon ++ {customerDetails.events.slice(0, 5).map((event) => ( + ++ + ))} ++ {format(new Date(event.createdAt), "dd.MM.yy HH.mm")} + ++ {event.name} + +{event.channel.name} ++ {event.icon} + ++ Discover the magic behind Propwrite's AI and how it turns ordinary listings + into captivating stories for potential buyers. + + +## Unveiling the Power of AI in Real Estate + +Propwrite's AI technology is redefining real estate listings, making them more than just a collection of features and photos. + +### AI-Enhanced Property Descriptions + +Our AI analyzes and enhances property details, weaving them into engaging narratives that resonate with buyers. + +#### Crafting the Perfect Narrative + +- **Data-Driven Storytelling:** Our AI uses data points to create stories that highlight the unique aspects of each property. +- **Tailored to Market Trends:** It dynamically adjusts to current market demands, ensuring your listing stands out. +- **Buyer-Centric Approach:** The AI crafts descriptions that appeal to the right audience, maximizing interest and engagement. + +##### Propwrite's AI at Work + +Here's a glimpse of how our AI transforms various aspects of a listing: + +| Aspect | AI Enhancement | +| ----------------- | ------------------------------------------------ | +| Property Features | Converts basic details into compelling stories | +| Market Insights | Adapts descriptions to current market trends | +| Buyer Preferences | Personalizes listings to attract the ideal buyer | + +Join the revolution in real estate with Propwrite. Embrace the future where AI drives success in every listing. + +For more insights or to join our community, reach out at [support@propwrite.com](mailto:support@propwrite.com) or connect with us on Discord [here](https://discord.gg/wadg6fNX). diff --git a/apps/www/src/content/blog/propwrite_aI_real_estate_blog_post.mdx b/apps/www/src/content/blog/propwrite_aI_real_estate_blog_post.mdx new file mode 100644 index 0000000..e349109 --- /dev/null +++ b/apps/www/src/content/blog/propwrite_aI_real_estate_blog_post.mdx @@ -0,0 +1,37 @@ +--- +title: "Boost Your Real Estate Business with Propwrite: The AI Advantage" +description: Discover how Propwrite leverages AI to revolutionize real estate, offering unparalleled efficiency and market insights. +image: /images/blog/blog-post-4.jpg +date: "2023-11-29" +authors: + - codehagen +--- + ++ Explore the transformative power of AI in real estate with Propwrite and see + how it can elevate your business to new heights. + + +## The AI Revolution in Real Estate + +The real estate market is experiencing a seismic shift, thanks to advancements in AI technology. Propwrite is at the forefront of this revolution, offering cutting-edge solutions to modernize and enhance real estate operations. + +### Transforming Listings with AI + +With Propwrite, real estate listings are no longer just a set of images and descriptions. They are transformed into engaging narratives, each crafted to highlight the unique appeal of a property, thanks to our advanced AI algorithms. + +#### Leveraging AI for Market Insights + +- **Advanced Data Analysis:** Propwrite's AI dives deep into market data, providing valuable insights that can inform your strategy and help you stay ahead of the curve. +- **Trend Forecasting:** Our AI tools are designed to identify and adapt to emerging market trends, ensuring your listings are always relevant and appealing. +- **Efficient Operations:** AI streamlines various aspects of real estate operations, from client communications to scheduling, increasing overall efficiency. + +##### Propwrite: A Game-Changer for Real Estate Professionals + +Propwrite's AI-driven platform is more than just a tool; it's a game-changer for real estate professionals. By automating mundane tasks and offering deep market insights, Propwrite enables agents to focus on what they do best โ selling properties. + +###### Connect with Propwrite Today + +Embrace the future of real estate with Propwrite. Join a growing community of professionals who are leveraging AI to transform their businesses. + +For more information or to join our community, contact us at [support@propwrite.com](mailto:support@propwrite.com) or connect on our Discord [here](https://discord.gg/wadg6fNX). diff --git a/apps/www/src/content/blog/welcome-to-propwrite.mdx b/apps/www/src/content/blog/welcome-to-propwrite.mdx new file mode 100644 index 0000000..775720a --- /dev/null +++ b/apps/www/src/content/blog/welcome-to-propwrite.mdx @@ -0,0 +1,81 @@ +--- +title: Welcome to Propwrite +description: Explore how Propwrite's AI is transforming real estate by enhancing listings and operations. +image: /images/blog/blog-post-2.jpg +date: "2023-11-22" +authors: + - codehagen +--- + ++ Christer is a real estate agent with a passion for coding. That's why he + created Propwrite - to make every real estate agent's job easier by letting AI + handle the heavy lifting of property listings. + + +Welcome to Propwrite, where AI meets real estate in an innovative blend thatโs transforming the industry. + +## Why AI in Real Estate? + +AI is reshaping real estate by bringing a new level of efficiency and insight. Itโs not just about automation; itโs about elevating the entire buying and selling experience. + +- AI doesnโt just process data; it uncovers opportunities. +- Think of it as a market whisperer, always staying ahead of trends. +- In short: Listings that not only list but also glisten. + +### Making Listings Stand Out with Propwrite + +Propwrite's AI-driven platform is more than just a tool; it's your partner in crafting listings that catch eyes and capture interest. With our AI, each property description becomes a compelling narrative, drawing potential buyers in from the first word. + +> "Our AI is like a real estate poet, creating listings that are not just informative but also inspiring," says Christer, the brain behind Propwrite. + +#### Streamlining Operations + +Propwrite goes beyond listings. Itโs about orchestrating the entire real estate operation seamlessly. From scheduling showings to managing client relationships, our AI is your all-in-one solution for a more organized, efficient, and effective real estate business. + +##### Connect with Us and Join the Community + +The future of real estate is here with Propwrite. We're not just building a product; we're cultivating a community. Join us on this exciting journey to reshape the world of real estate. + +Interested in more insights and networking with like-minded professionals? Join our Discord community [here](--- +title: Welcome to Propwrite +description: Explore how Propwrite's AI is transforming real estate by enhancing listings and operations. +image: /images/blog/blog-post-2.jpg +date: "2023-11-11" +authors: + +- codehagen + +--- + ++ Christer is a real estate agent with a passion for coding. That's why he + created Propwrite - to make every real estate agent's job easier by letting AI + handle the heavy lifting of property listings. + + +Welcome to Propwrite, where AI meets real estate in an innovative blend thatโs transforming the industry. + +## Why AI in Real Estate? + +AI is reshaping real estate by bringing a new level of efficiency and insight. Itโs not just about automation; itโs about elevating the entire buying and selling experience. + +- AI doesnโt just process data; it uncovers opportunities. +- Think of it as a market whisperer, always staying ahead of trends. +- In short: Listings that not only list but also glisten. + +### Making Listings Stand Out with Propwrite + +Propwrite's AI-driven platform is more than just a tool; it's your partner in crafting listings that catch eyes and capture interest. With our AI, each property description becomes a compelling narrative, drawing potential buyers in from the first word. + +> "Our AI is like a real estate poet, creating listings that are not just informative but also inspiring," says Christer, the brain behind Propwrite. + +#### Streamlining Operations + +PPropwrite isn't just revolutionizing listings; it's redefining the entire workflow of real estate operations. Our platform transforms the cumbersome tasks of managing a real estate business into a streamlined, intuitive process. + +##### Connect with Us and Join the Community + +The future of real estate is here with Propwrite. We're not just building a product; we're cultivating a community. Join us on this exciting journey to reshape the world of real estate. + +Interested in more insights and networking with like-minded professionals? Join our Discord community [here](https://discord.gg/wadg6fNX) and be a part of the real estate revolution. diff --git a/apps/www/src/content/docs/documentation/code-blocks.mdx b/apps/www/src/content/docs/documentation/code-blocks.mdx new file mode 100644 index 0000000..1bbe5a8 --- /dev/null +++ b/apps/www/src/content/docs/documentation/code-blocks.mdx @@ -0,0 +1,73 @@ +--- +title: Code Blocks +description: Advanced code blocks with highlighting, file names and more. +--- + +The code blocks on the documentation site and the blog are powered by [rehype-pretty-code](https://github.com/atomiks/rehype-pretty-code). The syntax highlighting is done using [shiki](https://github.com/shikijs/shiki). + +It has the following features: + +1. Beautiful code blocks with syntax highlighting. +2. Support for VS Code themes. +3. Works with hundreds of languages. +4. Line and word highlighting. +5. Support for line numbers. +6. Show code block titles using meta strings. + ++ +Thanks to Shiki, highlighting is done at build time. No JavaScript is sent to the client for runtime highlighting. + + + +## Example + +```ts showLineNumbers title="next.config.js" {3} /appDir: true/ +import { withContentlayer } from "next-contentlayer"; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + images: { + domains: ["avatars.githubusercontent.com"], + }, + experimental: { + appDir: true, + serverComponentsExternalPackages: ["@prisma/client"], + }, +}; + +export default withContentlayer(nextConfig); +``` + +## Title + +````mdx +```ts title="path/to/file.ts" +// Code here +``` +```` + +## Line Highlight + +````mdx +```ts {1,3-6} +// Highlight line 1 and line 3 to 6 +``` +```` + +## Word Highlight + +````mdx +```ts /shadcn/ +// Highlight the word shadcn. +``` +```` + +## Line Numbers + +````mdx +```ts showLineNumbers +// This will show line numbers. +``` +```` diff --git a/apps/www/src/content/docs/documentation/components.mdx b/apps/www/src/content/docs/documentation/components.mdx new file mode 100644 index 0000000..f56dbed --- /dev/null +++ b/apps/www/src/content/docs/documentation/components.mdx @@ -0,0 +1,157 @@ +--- +title: Components +description: Use React components in Markdown using MDX. +--- + +The following components are available out of the box for use in Markdown. + +If you'd like to build and add your own custom components, see the [Custom Components](#custom-components) section below. + +## Built-in Components + +### 1. Callout + +```mdx ++ +This is a default callout. You can embed **Markdown** inside a `callout`. + + +``` + ++ +This is a default callout. You can embed **Markdown** inside a `callout`. + + + ++ +This is a warning callout. It uses the props `type="warning"`. + + + ++ +This is a danger callout. It uses the props `type="danger"`. + + + +### 2. Card + +```mdx ++ +#### Heading + +You can use **markdown** inside cards. + + +``` + ++ +#### Heading + +You can use **markdown** inside cards. + + + +You can also use HTML to embed cards in a grid. + +```mdx +++``` + ++ #### Card One + You can use **markdown** inside cards. + + ++ #### Card Two + You can also use `inline code` and code blocks. + +++ +--- + +## Custom Components + +You can add your own custom components using the `components` props from `useMDXComponent`. + +```ts title="components/mdx.tsx" {2,6} +import { Callout } from "@/components/callout" +import { CustomComponent } from "@/components/custom" + +const components = { + Callout, + CustomComponent, +} + +export function Mdx({ code }) { + const Component = useMDXComponent(code) + + return ( ++ #### Card One + You can use **markdown** inside cards. + + ++ #### Card Two + You can also use `inline code` and code blocks. + +++ ) +} +``` + +Once you've added your custom component, you can use it in MDX as follows: + +```js ++ +``` + +--- + +## HTML Elements + +You can overwrite HTML elements using the same technique above. + +```ts {4} +const components = { + Callout, + CustomComponent, + hr: ({ ...props }) =>
, +} +``` + +This will overwrite the `
` tag or `---` in Mardown with the HTML output above. + +--- + +## Styling + +Tailwind CSS classes can be used inside MDX for styling. + +```mdx +This text will be red.
+``` + +Make sure you have configured the path to your content in your `tailwind.config.js` file: + +```js title="tailwind.config.js" {6} +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./app/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./content/**/*.{md,mdx}", + ], +}; +``` diff --git a/apps/www/src/content/docs/documentation/index.mdx b/apps/www/src/content/docs/documentation/index.mdx new file mode 100644 index 0000000..c3e6b1d --- /dev/null +++ b/apps/www/src/content/docs/documentation/index.mdx @@ -0,0 +1,60 @@ +--- +title: Documentation +description: Build your documentation site using Contentlayer and MDX. +--- + +Taxonomy includes a documentation site built using [Contentlayer](https://contentlayer.dev) and [MDX](http://mdxjs.com). + +## Features + +It comes with the following features out of the box: + +1. Write content using MDX. +2. Transform and validate content using Contentlayer. +3. MDX components such as `` and ` `. +4. Support for table of contents. +5. Custom navigation with prev and next pager. +6. Beautiful code blocks using `rehype-pretty-code`. +7. Syntax highlighting using `shiki`. +8. Built-in search (_in progress_). +9. Dark mode (_in progress_). + +## How is it built + +Click on a section below to learn how the documentation site built. + + + +diff --git a/apps/www/src/content/docs/documentation/style-guide.mdx b/apps/www/src/content/docs/documentation/style-guide.mdx new file mode 100644 index 0000000..830af07 --- /dev/null +++ b/apps/www/src/content/docs/documentation/style-guide.mdx @@ -0,0 +1,216 @@ +--- +title: Style Guide +description: Testing the MDX style guide with Tailwind Typography +--- + ++ +### Contentlayer + +Learn how to use MDX with Contentlayer. + + + ++ +### Components + +Using React components in Mardown. + + + ++ +### Code Blocks + +Beautiful code blocks with syntax highlighting. + + + ++ +### Style Guide + +View a sample page with all the styles. + + + ++ +- The text below is from the [Tailwind CSS](https://play.tailwindcss.com/uj1vGACRJA?layout=preview) docs. I copied it here to test the markdown styles. **Tailwind is awesome. You should use it.** +- The CSS is from MDX sites I've built through the years. I copied this from [Nextra](https://github.com/shuding/nextra) and tweaked it a bit to fit the styles of this site. + + + +Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS. + +By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. + +We get lots of complaints about it actually, with people regularly asking us things like: + +> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? +> We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either โ you want them to look _awesome_, not awful. + +The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. + +It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: + +```html ++ +``` + +For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). + +--- + +## What to expect from here on out + +What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. + +It's important to cover all of these use cases for a few reasons: + +1. We want everything to look good out of the box. +2. Really just the first reason, that's the whole point of the plugin. +3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. + +Now we're going to try out another header style. + +### Typography should be easy + +So that's a header for you โ with any luck if we've done our job correctly that will look pretty reasonable. + +Something a wise person once told me about typography is: + +> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. + +It's probably important that images look okay here by default as well: + +Garlic bread with cheese: What the science tells us
++ For years parents have espoused the health benefits of eating garlic bread + with cheese to their children, with the food earning such an iconic status + in our culture that kids will often dress up as warm, cheesy loaf for + Halloween. +
++ But a recent study shows that the celebrated appetizer may be linked to a + series of rabies cases springing up around the country. +
++ +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. + +Now I'm going to show you an example of an unordered list to make sure that looks good, too: + +- So here is the first item in this list. +- In this example we're keeping the items short. +- Later, we'll use longer, more complex list items. + +And that's the end of this section. + +## What if we stack headings? + +### We should make sure that looks good, too. + +Sometimes you have headings directly underneath each other. In those cases you often have to undo the top margin on the second heading because it usually looks better for the headings to be closer together than a paragraph followed by a heading should be. + +### When a heading comes after a paragraph โฆ + +When a heading comes after a paragraph, we need a bit more space, like I already mentioned above. Now let's see what a more complex list would look like. + +- **I often do this thing where list items have headings.** + + For some reason I think this looks cool which is unfortunate because it's pretty annoying to get the styles right. + + I often have two or three paragraphs in these list items, too, so the hard part is getting the spacing between the paragraphs, list item heading, and separate list items to all make sense. Pretty tough honestly, you could make a strong argument that you just shouldn't write this way. + +- **Since this is a list, I need at least two items.** + + I explained what I'm doing already in the previous list item, but a list wouldn't be a list if it only had one item, and we really want this to look realistic. That's why I've added this second list item so I actually have something to look at when writing the styles. + +- **It's not a bad idea to add a third item either.** + + I think it probably would've been fine to just use two items but three is definitely not worse, and since I seem to be having no trouble making up arbitrary things to type, I might as well include it. + +After this sort of list I usually have a closing statement or paragraph, because it kinda looks weird jumping right to a heading. + +## Code should look okay by default. + +I think most people are going to use [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/) or something if they want to style their code blocks but it wouldn't hurt to make them look _okay_ out of the box, even with no syntax highlighting. + +Here's what a default `tailwind.config.js` file looks like at the time of writing: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +Hopefully that looks good enough to you. + +### What about nested lists? + +Nested lists basically always look bad which is why editors like Medium don't even let you do it, but I guess since some of you goofballs are going to do it we have to carry the burden of at least making it work. + +1. **Nested lists are rarely a good idea.** + - You might feel like you are being really "organized" or something but you are just creating a gross shape on the screen that is hard to read. + - Nested navigation in UIs is a bad idea too, keep things as flat as possible. + - Nesting tons of folders in your source code is also not helpful. +2. **Since we need to have more items, here's another one.** + - I'm not sure if we'll bother styling more than two levels deep. + - Two is already too much, three is guaranteed to be a bad idea. + - If you nest four levels deep you belong in prison. +3. **Two items isn't really a list, three is good though.** + - Again please don't nest lists if you want people to actually read your content. + - Nobody wants to look at this. + - I'm upset that we even have to bother styling this. + +The most annoying thing about lists in Markdown is that ` ` elements aren't given a child ` ` tag unless there are multiple paragraphs in the list item. That means I have to worry about styling that annoying situation too. + +- **For example, here's another nested list.** + + But this time with a second paragraph. + + - These list items won't have `
` tags + - Because they are only one line each + +- **But in this second top-level list item, they will.** + + This is especially annoying because of the spacing on this paragraph. + + - As you can see here, because I've added a second line, this list item now has a `
` tag. + + This is the second line I'm talking about by the way. + + - Finally here's another list item so it's more like a list. + +- A closing list item, but with no nested list, because why not? + +And finally a sentence to close off this section. + +## There are other elements we need to style + +I almost forgot to mention links, like [this link to the Tailwind CSS website](https://tailwindcss.com). We almost made them blue but that's so yesterday, so we went with dark gray, feels edgier. + +We even included table styles, check it out: + +| Wrestler | Origin | Finisher | +| ----------------------- | ------------ | ------------------ | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +We also need to make sure inline code looks good, like if I wanted to talk about `` elements or tell you the good news about `@tailwindcss/typography`. + +### Sometimes I even use `code` in headings + +Even though it's probably a bad idea, and historically I've had a hard time making it look good. This _"wrap the code blocks in backticks"_ trick works pretty well though really. + +Another thing I've done in the past is put a `code` tag inside of a link, like if I wanted to tell you about the [`tailwindcss/docs`](https://github.com/tailwindcss/docs) repository. I don't love that there is an underline below the backticks but it is absolutely not worth the madness it would require to avoid it. + +#### We haven't used an `h4` yet + +But now we have. Please don't use `h5` or `h6` in your content, Medium only supports two heading levels for a reason, you animals. I honestly considered using a `before` pseudo-element to scream at you if you use an `h5` or `h6`. + +We don't style them at all out of the box because `h4` elements are already so small that they are the same size as the body copy. What are we supposed to do with an `h5`, make it _smaller_ than the body copy? No thanks. + +### We still need to think about stacked headings though. + +#### Let's make sure we don't screw that up with `h4` elements, either. + +Phew, with any luck we have styled the headings above this text and they look pretty good. + +Let's add a closing paragraph here so things end with a decently sized block of text. I can't explain why I want things to end that way but I have to assume it's because I think things will look weird or unbalanced if there is a heading too close to the end of the document. + +What I've written here is probably long enough, but adding this final sentence can't hurt. + +## GitHub Flavored Markdown + +I've also added support for GitHub Flavored Mardown using `remark-gfm`. + +With `remark-gfm`, we get a few extra features in our markdown. Example: autolink literals. + +A link like www.example.com or https://example.com would automatically be converted into an `a` tag. + +This works for email links too: contact@example.com. diff --git a/apps/www/src/content/docs/in-progress.mdx b/apps/www/src/content/docs/in-progress.mdx new file mode 100644 index 0000000..c48c938 --- /dev/null +++ b/apps/www/src/content/docs/in-progress.mdx @@ -0,0 +1,10 @@ +--- +title: Not Implemented +description: This page is in progress. +--- + +
+ +This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn). + + diff --git a/apps/www/src/content/docs/index.mdx b/apps/www/src/content/docs/index.mdx new file mode 100644 index 0000000..8f12d68 --- /dev/null +++ b/apps/www/src/content/docs/index.mdx @@ -0,0 +1,54 @@ +--- +title: Documentation +description: Welcome to the Taxonomy documentation. +--- + +This is the documentation for the Taxonomy site. + +This is an example of a doc site built using [ContentLayer](/docs/documentation/contentlayer) and MDX. + ++ +This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn). + + + +## Features + +Select a feature below to learn more about it. + ++ +diff --git a/apps/www/src/content/guides/build-blog-using-contentlayer-mdx.mdx b/apps/www/src/content/guides/build-blog-using-contentlayer-mdx.mdx new file mode 100644 index 0000000..d9c0bc5 --- /dev/null +++ b/apps/www/src/content/guides/build-blog-using-contentlayer-mdx.mdx @@ -0,0 +1,222 @@ +--- +title: Build a blog using ContentLayer and MDX. +description: Learn how to use ContentLayer to build a blog with Next.js +date: 2022-11-18 +--- + ++ +### Documentation + +This documentation site built using Contentlayer. + + + ++ +### Marketing + +The marketing site with landing pages. + + + ++ +### App + +The dashboard with auth and subscriptions. + + + ++ +### Blog + +The blog built using Contentlayer and MDX. + + + ++ +This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn). + + + ++ The text below is from the [Tailwind + CSS](https://play.tailwindcss.com/uj1vGACRJA?layout=preview) docs. I copied it + here to test the markdown styles. **Tailwind is awesome. You should use it.** + + +Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS. + +By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. + +We get lots of complaints about it actually, with people regularly asking us things like: + +> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? +> We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either โ you want them to look _awesome_, not awful. + +The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. + +It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: + +```html ++ +``` + +For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). + +--- + +## What to expect from here on out + +What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. + +It's important to cover all of these use cases for a few reasons: + +1. We want everything to look good out of the box. +2. Really just the first reason, that's the whole point of the plugin. +3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. + +Now we're going to try out another header style. + +### Typography should be easy + +So that's a header for you โ with any luck if we've done our job correctly that will look pretty reasonable. + +Something a wise person once told me about typography is: + +> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. + +It's probably important that images look okay here by default as well: + +Garlic bread with cheese: What the science tells us
++ For years parents have espoused the health benefits of eating garlic bread + with cheese to their children, with the food earning such an iconic status + in our culture that kids will often dress up as warm, cheesy loaf for + Halloween. +
++ But a recent study shows that the celebrated appetizer may be linked to a + series of rabies cases springing up around the country. +
++ +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. + +Now I'm going to show you an example of an unordered list to make sure that looks good, too: + +- So here is the first item in this list. +- In this example we're keeping the items short. +- Later, we'll use longer, more complex list items. + +And that's the end of this section. + +## What if we stack headings? + +### We should make sure that looks good, too. + +Sometimes you have headings directly underneath each other. In those cases you often have to undo the top margin on the second heading because it usually looks better for the headings to be closer together than a paragraph followed by a heading should be. + +### When a heading comes after a paragraph โฆ + +When a heading comes after a paragraph, we need a bit more space, like I already mentioned above. Now let's see what a more complex list would look like. + +- **I often do this thing where list items have headings.** + + For some reason I think this looks cool which is unfortunate because it's pretty annoying to get the styles right. + + I often have two or three paragraphs in these list items, too, so the hard part is getting the spacing between the paragraphs, list item heading, and separate list items to all make sense. Pretty tough honestly, you could make a strong argument that you just shouldn't write this way. + +- **Since this is a list, I need at least two items.** + + I explained what I'm doing already in the previous list item, but a list wouldn't be a list if it only had one item, and we really want this to look realistic. That's why I've added this second list item so I actually have something to look at when writing the styles. + +- **It's not a bad idea to add a third item either.** + + I think it probably would've been fine to just use two items but three is definitely not worse, and since I seem to be having no trouble making up arbitrary things to type, I might as well include it. + +After this sort of list I usually have a closing statement or paragraph, because it kinda looks weird jumping right to a heading. + +## Code should look okay by default. + +I think most people are going to use [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/) or something if they want to style their code blocks but it wouldn't hurt to make them look _okay_ out of the box, even with no syntax highlighting. + +Here's what a default `tailwind.config.js` file looks like at the time of writing: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +Hopefully that looks good enough to you. + +### What about nested lists? + +Nested lists basically always look bad which is why editors like Medium don't even let you do it, but I guess since some of you goofballs are going to do it we have to carry the burden of at least making it work. + +1. **Nested lists are rarely a good idea.** + - You might feel like you are being really "organized" or something but you are just creating a gross shape on the screen that is hard to read. + - Nested navigation in UIs is a bad idea too, keep things as flat as possible. + - Nesting tons of folders in your source code is also not helpful. +2. **Since we need to have more items, here's another one.** + - I'm not sure if we'll bother styling more than two levels deep. + - Two is already too much, three is guaranteed to be a bad idea. + - If you nest four levels deep you belong in prison. +3. **Two items isn't really a list, three is good though.** + - Again please don't nest lists if you want people to actually read your content. + - Nobody wants to look at this. + - I'm upset that we even have to bother styling this. + +The most annoying thing about lists in Markdown is that ` ` elements aren't given a child ` ` tag unless there are multiple paragraphs in the list item. That means I have to worry about styling that annoying situation too. + +- **For example, here's another nested list.** + + But this time with a second paragraph. + + - These list items won't have `
` tags + - Because they are only one line each + +- **But in this second top-level list item, they will.** + + This is especially annoying because of the spacing on this paragraph. + + - As you can see here, because I've added a second line, this list item now has a `
` tag. + + This is the second line I'm talking about by the way. + + - Finally here's another list item so it's more like a list. + +- A closing list item, but with no nested list, because why not? + +And finally a sentence to close off this section. + +## There are other elements we need to style + +I almost forgot to mention links, like [this link to the Tailwind CSS website](https://tailwindcss.com). We almost made them blue but that's so yesterday, so we went with dark gray, feels edgier. + +We even included table styles, check it out: + +| Wrestler | Origin | Finisher | +| ----------------------- | ------------ | ------------------ | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +We also need to make sure inline code looks good, like if I wanted to talk about `` elements or tell you the good news about `@tailwindcss/typography`. + +### Sometimes I even use `code` in headings + +Even though it's probably a bad idea, and historically I've had a hard time making it look good. This _"wrap the code blocks in backticks"_ trick works pretty well though really. + +Another thing I've done in the past is put a `code` tag inside of a link, like if I wanted to tell you about the [`tailwindcss/docs`](https://github.com/tailwindcss/docs) repository. I don't love that there is an underline below the backticks but it is absolutely not worth the madness it would require to avoid it. + +#### We haven't used an `h4` yet + +But now we have. Please don't use `h5` or `h6` in your content, Medium only supports two heading levels for a reason, you animals. I honestly considered using a `before` pseudo-element to scream at you if you use an `h5` or `h6`. + +We don't style them at all out of the box because `h4` elements are already so small that they are the same size as the body copy. What are we supposed to do with an `h5`, make it _smaller_ than the body copy? No thanks. + +### We still need to think about stacked headings though. + +#### Let's make sure we don't screw that up with `h4` elements, either. + +Phew, with any luck we have styled the headings above this text and they look pretty good. + +Let's add a closing paragraph here so things end with a decently sized block of text. I can't explain why I want things to end that way but I have to assume it's because I think things will look weird or unbalanced if there is a heading too close to the end of the document. + +What I've written here is probably long enough, but adding this final sentence can't hurt. + +## GitHub Flavored Markdown + +I've also added support for GitHub Flavored Mardown using `remark-gfm`. + +With `remark-gfm`, we get a few extra features in our markdown. Example: autolink literals. + +A link like www.example.com or https://example.com would automatically be converted into an `a` tag. + +This works for email links too: contact@example.com. diff --git a/apps/www/src/content/guides/using-next-auth-next-13.mdx b/apps/www/src/content/guides/using-next-auth-next-13.mdx new file mode 100644 index 0000000..956776d --- /dev/null +++ b/apps/www/src/content/guides/using-next-auth-next-13.mdx @@ -0,0 +1,222 @@ +--- +title: Using NextAuth.js with Next.13 +description: How to use NextAuth.js in server components. +date: 2022-11-23 +--- + +
+ +This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn). + + + ++ The text below is from the [Tailwind + CSS](https://play.tailwindcss.com/uj1vGACRJA?layout=preview) docs. I copied it + here to test the markdown styles. **Tailwind is awesome. You should use it.** + + +Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS. + +By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. + +We get lots of complaints about it actually, with people regularly asking us things like: + +> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? +> We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either โ you want them to look _awesome_, not awful. + +The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. + +It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: + +```html ++ +``` + +For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). + +--- + +## What to expect from here on out + +What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. + +It's important to cover all of these use cases for a few reasons: + +1. We want everything to look good out of the box. +2. Really just the first reason, that's the whole point of the plugin. +3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. + +Now we're going to try out another header style. + +### Typography should be easy + +So that's a header for you โ with any luck if we've done our job correctly that will look pretty reasonable. + +Something a wise person once told me about typography is: + +> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. + +It's probably important that images look okay here by default as well: + +Garlic bread with cheese: What the science tells us
++ For years parents have espoused the health benefits of eating garlic bread + with cheese to their children, with the food earning such an iconic status + in our culture that kids will often dress up as warm, cheesy loaf for + Halloween. +
++ But a recent study shows that the celebrated appetizer may be linked to a + series of rabies cases springing up around the country. +
++ +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. + +Now I'm going to show you an example of an unordered list to make sure that looks good, too: + +- So here is the first item in this list. +- In this example we're keeping the items short. +- Later, we'll use longer, more complex list items. + +And that's the end of this section. + +## What if we stack headings? + +### We should make sure that looks good, too. + +Sometimes you have headings directly underneath each other. In those cases you often have to undo the top margin on the second heading because it usually looks better for the headings to be closer together than a paragraph followed by a heading should be. + +### When a heading comes after a paragraph โฆ + +When a heading comes after a paragraph, we need a bit more space, like I already mentioned above. Now let's see what a more complex list would look like. + +- **I often do this thing where list items have headings.** + + For some reason I think this looks cool which is unfortunate because it's pretty annoying to get the styles right. + + I often have two or three paragraphs in these list items, too, so the hard part is getting the spacing between the paragraphs, list item heading, and separate list items to all make sense. Pretty tough honestly, you could make a strong argument that you just shouldn't write this way. + +- **Since this is a list, I need at least two items.** + + I explained what I'm doing already in the previous list item, but a list wouldn't be a list if it only had one item, and we really want this to look realistic. That's why I've added this second list item so I actually have something to look at when writing the styles. + +- **It's not a bad idea to add a third item either.** + + I think it probably would've been fine to just use two items but three is definitely not worse, and since I seem to be having no trouble making up arbitrary things to type, I might as well include it. + +After this sort of list I usually have a closing statement or paragraph, because it kinda looks weird jumping right to a heading. + +## Code should look okay by default. + +I think most people are going to use [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/) or something if they want to style their code blocks but it wouldn't hurt to make them look _okay_ out of the box, even with no syntax highlighting. + +Here's what a default `tailwind.config.js` file looks like at the time of writing: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +Hopefully that looks good enough to you. + +### What about nested lists? + +Nested lists basically always look bad which is why editors like Medium don't even let you do it, but I guess since some of you goofballs are going to do it we have to carry the burden of at least making it work. + +1. **Nested lists are rarely a good idea.** + - You might feel like you are being really "organized" or something but you are just creating a gross shape on the screen that is hard to read. + - Nested navigation in UIs is a bad idea too, keep things as flat as possible. + - Nesting tons of folders in your source code is also not helpful. +2. **Since we need to have more items, here's another one.** + - I'm not sure if we'll bother styling more than two levels deep. + - Two is already too much, three is guaranteed to be a bad idea. + - If you nest four levels deep you belong in prison. +3. **Two items isn't really a list, three is good though.** + - Again please don't nest lists if you want people to actually read your content. + - Nobody wants to look at this. + - I'm upset that we even have to bother styling this. + +The most annoying thing about lists in Markdown is that ` ` elements aren't given a child ` +` tag unless there are multiple paragraphs in the list item. That means I have to worry about styling that annoying situation too. + +- **For example, here's another nested list.** + + But this time with a second paragraph. + + - These list items won't have `
` tags + - Because they are only one line each + +- **But in this second top-level list item, they will.** + + This is especially annoying because of the spacing on this paragraph. + + - As you can see here, because I've added a second line, this list item now has a `
` tag. + + This is the second line I'm talking about by the way. + + - Finally here's another list item so it's more like a list. + +- A closing list item, but with no nested list, because why not? + +And finally a sentence to close off this section. + +## There are other elements we need to style + +I almost forgot to mention links, like [this link to the Tailwind CSS website](https://tailwindcss.com). We almost made them blue but that's so yesterday, so we went with dark gray, feels edgier. + +We even included table styles, check it out: + +| Wrestler | Origin | Finisher | +| ----------------------- | ------------ | ------------------ | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +We also need to make sure inline code looks good, like if I wanted to talk about `` elements or tell you the good news about `@tailwindcss/typography`. + +### Sometimes I even use `code` in headings + +Even though it's probably a bad idea, and historically I've had a hard time making it look good. This _"wrap the code blocks in backticks"_ trick works pretty well though really. + +Another thing I've done in the past is put a `code` tag inside of a link, like if I wanted to tell you about the [`tailwindcss/docs`](https://github.com/tailwindcss/docs) repository. I don't love that there is an underline below the backticks but it is absolutely not worth the madness it would require to avoid it. + +#### We haven't used an `h4` yet + +But now we have. Please don't use `h5` or `h6` in your content, Medium only supports two heading levels for a reason, you animals. I honestly considered using a `before` pseudo-element to scream at you if you use an `h5` or `h6`. + +We don't style them at all out of the box because `h4` elements are already so small that they are the same size as the body copy. What are we supposed to do with an `h5`, make it _smaller_ than the body copy? No thanks. + +### We still need to think about stacked headings though. + +#### Let's make sure we don't screw that up with `h4` elements, either. + +Phew, with any luck we have styled the headings above this text and they look pretty good. + +Let's add a closing paragraph here so things end with a decently sized block of text. I can't explain why I want things to end that way but I have to assume it's because I think things will look weird or unbalanced if there is a heading too close to the end of the document. + +What I've written here is probably long enough, but adding this final sentence can't hurt. + +## GitHub Flavored Markdown + +I've also added support for GitHub Flavored Mardown using `remark-gfm`. + +With `remark-gfm`, we get a few extra features in our markdown. Example: autolink literals. + +A link like www.example.com or https://example.com would automatically be converted into an `a` tag. + +This works for email links too: contact@example.com. diff --git a/apps/www/src/content/pages/privacy.mdx b/apps/www/src/content/pages/privacy.mdx new file mode 100644 index 0000000..6390468 --- /dev/null +++ b/apps/www/src/content/pages/privacy.mdx @@ -0,0 +1,45 @@ +--- +title: Privacy +description: The Privacy Policy for Propwrite +--- + +# Privacy Policy + +At Propwrite, we are committed to protecting your privacy and ensuring that your personal information is handled in a safe and responsible manner. This Privacy Policy outlines how we collect, use, store, and protect the information you provide us when using our services. + +## Consent + +By using our website and services, you agree to the collection and use of information in accordance with this policy. If you do not agree with our policies and practices, please do not use our website. + +## Information We Collect + +We collect several types of information from and about users of our website, including: + +- **Personal Data**: This may include, but is not limited to, your name, email address, postal address, phone number, and other identifiers by which you may be contacted online or offline. +- **Usage Data**: We may also collect information on how the website is accessed and used. This data may include your computer's Internet Protocol address (IP address), browser type, browser version, the pages you visit, the time and date of your visit, the time spent on those pages, and other diagnostic data. + +## How We Use Your Information + +We use the information we collect about you or that you provide to us, including any personal data, for various purposes, including to: + +- Provide and maintain our website. +- Notify you about changes to our website or any products or services we offer or provide through it. +- Allow you to participate in interactive features on our website. +- Provide customer support. +- Gather analysis or valuable information so that we can improve our website. +- Monitor the usage of our website. +- Detect, prevent and address technical issues. + +## Data Security + +We have implemented measures designed to secure your personal information from accidental loss and from unauthorized access, use, alteration, and disclosure. However, the transmission of information via the internet is not completely secure. Although we do our best to protect your personal information, we cannot guarantee the security of your personal information transmitted to our website. + +## Changes to Our Privacy Policy + +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. We will let you know via email and/or a prominent notice on our website, prior to the change becoming effective. + +## Contact Information + +To ask questions or comment about this Privacy Policy and our privacy practices, contact us at: [your contact information or email]. + +Last updated: 28.11.2023 diff --git a/apps/www/src/content/pages/terms.mdx b/apps/www/src/content/pages/terms.mdx new file mode 100644 index 0000000..e53263c --- /dev/null +++ b/apps/www/src/content/pages/terms.mdx @@ -0,0 +1,30 @@ +--- +title: Terms & Conditions +description: Read our terms and conditions. +--- + +Lorem ipsumMagna fermentum iaculis eu non diam. Vitae purus faucibus ornare suspendisse sed nisi lacus sed. In nibh mauris cursus mattis molestie a iaculis at. Enim sit amet venenatis urna. Eget sit amet tellus cras adipiscing. + +## Legal Notices + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Volutpat sed cras ornare arcu. Nibh ipsum consequat nisl vel pretium lectus quam id leo. A arcu cursus vitae congue. Amet justo donec enim diam. Vel pharetra vel turpis nunc eget lorem. Gravida quis blandit turpis cursus in. Semper auctor neque vitae tempus. Elementum facilisis leo vel fringilla est ullamcorper eget nulla. Imperdiet nulla malesuada pellentesque elit eget. + +Felis donec et odio pellentesque diam volutpat commodo sed. + +Tortor consequat id porta nibh. Fames ac turpis egestas maecenas pharetra convallis posuere morbi leo. Scelerisque fermentum dui faucibus in. Tortor posuere ac ut consequat semper viverra. + +## Warranty Disclaimer + +Tellus in hac habitasse platea dictumst vestibulum. Faucibus in ornare quam viverra. Viverra aliquet eget sit amet tellus cras adipiscing. Erat nam at lectus urna duis convallis convallis tellus. Bibendum est ultricies integer quis auctor elit sed vulputate. + +Nisl condimentum id venenatis a condimentum vitae. Ac auctor augue mauris augue neque gravida in fermentum. Arcu felis bibendum ut tristique. Tempor commodo ullamcorper a lacus vestibulum sed arcu non. + +## General + +Magna fermentum iaculis eu non diam. Vitae purus faucibus ornare suspendisse sed nisi lacus sed. In nibh mauris cursus mattis molestie a iaculis at. Enim sit amet venenatis urna. Eget sit amet tellus cras adipiscing. + +Sed lectus vestibulum mattis ullamcorper velit. Id diam vel quam elementum pulvinar. In iaculis nunc sed augue lacus viverra. In hendrerit gravida rutrum quisque non tellus. Nisl purus in mollis nunc. + +## Disclaimer + +Amet justo donec enim diam. In hendrerit gravida rutrum quisque non. Hac habitasse platea dictumst quisque sagittis purus sit. Faucibus ornare suspendisse sed nisi lacus. Nulla porttitor massa id neque aliquam vestibulum. Ante in nibh mauris cursus mattis molestie a. Mi tempus imperdiet nulla malesuada. diff --git a/apps/www/src/emails/components/footer.tsx b/apps/www/src/emails/components/footer.tsx new file mode 100644 index 0000000..cd0f0b2 --- /dev/null +++ b/apps/www/src/emails/components/footer.tsx @@ -0,0 +1,35 @@ +/* eslint-disable react/no-unescaped-entities */ +import { Hr, Tailwind, Text } from "@react-email/components"; + +export default function Footer({ + email, + marketing, +}: { + email: string; + marketing?: boolean; +}) { + return ( +
+ + ); +} diff --git a/apps/www/src/emails/magic-link-email.tsx b/apps/www/src/emails/magic-link-email.tsx new file mode 100644 index 0000000..94fe41e --- /dev/null +++ b/apps/www/src/emails/magic-link-email.tsx @@ -0,0 +1,70 @@ +import { + Body, + Button, + Container, + Head, + Hr, + Html, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; + +import { Icons } from "../components/shared/icons"; + +type MagicLinkEmailProps = { + actionUrl: string; + firstName: string; + mailType: "login" | "register"; + siteName: string; +}; + +export const MagicLinkEmail = ({ + firstName = "", + actionUrl, + mailType, + siteName, +}: MagicLinkEmailProps) => ( + + +
+ {marketing ? ( ++ This email was intended for{" "} + {email}. If you were not expecting + this email, you can ignore this email. If you don't want to receive + emails like this in the future, you can{" "} + + unsubscribe here + + . + + ) : ( ++ This email was intended for{" "} + {email}. If you were not expecting + this email, you can ignore this email. If you are concerned about your + account's safety, please reply to this email to get in touch with us. + + )} ++ The sales intelligence platform that helps you uncover qualified leads. + ++ + + +); + +export default MagicLinkEmail; diff --git a/apps/www/src/emails/welcome-email.tsx b/apps/www/src/emails/welcome-email.tsx new file mode 100644 index 0000000..019eaf1 --- /dev/null +++ b/apps/www/src/emails/welcome-email.tsx @@ -0,0 +1,89 @@ +/* eslint-disable react/no-unescaped-entities */ +import { + Body, + Container, + Head, + Heading, + Hr, + Html, + Img, + Link, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; + +import Footer from "./components/footer"; + +export default function WelcomeEmail({ + name = "John Doe", + email = "welcome@propwrite.com", +}: { + name: string | null; + email: string; +}) { + return ( + ++ + ++ Hi {firstName}, ++ Welcome to {siteName} ! Click the link below to{" "} + {mailType === "login" ? "sign in to" : "activate"} your account. + ++ + ++ This link expires in 24 hours and can only be used once. + + {mailType === "login" ? ( ++ If you did not try to log into your account, you can safely ignore + it. + + ) : null} +
++ 123 Code Street, Suite 404, Devtown, CA 98765 + +Welcome to Propwrite ++ + + + ); +} diff --git a/apps/www/src/env.ts b/apps/www/src/env.ts new file mode 100644 index 0000000..8e4fe10 --- /dev/null +++ b/apps/www/src/env.ts @@ -0,0 +1,48 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + server: { + // This is optional because it's only used in development. + // See https://next-auth.js.org/deployment. + // GITHUB_OAUTH_TOKEN: z.string().min(1), + NEXTAUTH_URL: z.string().url().optional(), + NEXTAUTH_SECRET: z.string().min(1), + GOOGLE_CLIENT_ID: z.string().min(1), + GOOGLE_CLIENT_SECRET: z.string().min(1), + DATABASE_URL: z.string().min(1), + RESEND_API_KEY: z.string().min(1), + STRIPE_API_KEY: z.string().min(1), + STRIPE_WEBHOOK_SECRET: z.string().min(1), + NODE_ENV: z.enum(["development", "test", "production"]), + }, + client: { + NEXT_PUBLIC_APP_URL: z.string().min(1), + NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: z.string().min(1), + NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: z.string().min(1), + NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID: z.string().min(1), + NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID: z.string().min(1), + }, + runtimeEnv: { + // GITHUB_OAUTH_TOKEN: process.env.GITHUB_OAUTH_TOKEN, + NEXTAUTH_URL: process.env.NEXTAUTH_URL, + NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, + DATABASE_URL: process.env.DATABASE_URL, + RESEND_API_KEY: process.env.RESEND_API_KEY, + NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, + // Stripe + STRIPE_API_KEY: process.env.STRIPE_API_KEY, + STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, + NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: + process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID, + NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: + process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID, + NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID: + process.env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID, + NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID: + process.env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID, + NODE_ENV: process.env.NODE_ENV, + }, +}); diff --git a/apps/www/src/get-user-channels.ts b/apps/www/src/get-user-channels.ts new file mode 100644 index 0000000..f098162 --- /dev/null +++ b/apps/www/src/get-user-channels.ts @@ -0,0 +1,33 @@ +// actions/get-user-channels.js +"use server"; + +import { prisma } from "@/lib/db"; +import { getCurrentUser } from "@/lib/session"; + +export async function getUserChannels() { + const user = await getCurrentUser(); + const userId = user?.id; + + if (!userId) { + console.error("No user is currently logged in."); + return { success: false, error: "User not authenticated" }; + } + + try { + const channels = await prisma.channel.findMany({ + where: { + project: { + userId: userId, + }, + }, + include: { + project: true, + }, + }); + + return { success: true, channels }; + } catch (error) { + console.error(`Error fetching channels for user ID: ${userId}`, error); + return { success: false, error: error.message }; + } +} diff --git a/apps/www/src/hooks/use-intersection-observer.ts b/apps/www/src/hooks/use-intersection-observer.ts new file mode 100644 index 0000000..516428d --- /dev/null +++ b/apps/www/src/hooks/use-intersection-observer.ts @@ -0,0 +1,43 @@ +import { RefObject, useEffect, useState } from "react"; + +interface Args extends IntersectionObserverInit { + freezeOnceVisible?: boolean; +} + +function useIntersectionObserver( + elementRef: RefObject+ + + ++ Welcome to Propwrite + + ++ Thank you for joining us{name && `, ${name}`}! + ++ I'm Christer, the creator of Propwrite, your new AI assistant for + real estate listings. Excited to have you on board! + ++ Here's what you can start doing: + ++ โ Generate your first{" "} + + AI-powered property listing + + ++ โ Manage your{" "} + + property portfolio + + ++ โ Connect with us on{" "} + + Twitter + + ++ If you have any questions or feedback, feel free to reach out. + We're here to help! + ++ Christer from Propwrite + + + +, + { + threshold = 0, + root = null, + rootMargin = "0%", + freezeOnceVisible = false, + }: Args, +): IntersectionObserverEntry | undefined { + const [entry, setEntry] = useState (); + + const frozen = entry?.isIntersecting && freezeOnceVisible; + + const updateEntry = ([entry]: IntersectionObserverEntry[]): void => { + setEntry(entry); + }; + + useEffect(() => { + const node = elementRef?.current; // DOM Ref + const hasIOSupport = !!window.IntersectionObserver; + + if (!hasIOSupport || frozen || !node) return; + + const observerParams = { threshold, root, rootMargin }; + const observer = new IntersectionObserver(updateEntry, observerParams); + + observer.observe(node); + + return () => observer.disconnect(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [threshold, root, rootMargin, frozen]); + + return entry; +} + +export default useIntersectionObserver; diff --git a/apps/www/src/hooks/use-language-modal.ts b/apps/www/src/hooks/use-language-modal.ts new file mode 100644 index 0000000..555daf0 --- /dev/null +++ b/apps/www/src/hooks/use-language-modal.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface UseLanguageModalStore { + isOpen: boolean; + onOpen: () => void; + onClose: () => void; +} + +export const useLanguageModal = create ((set) => ({ + isOpen: false, + onOpen: () => set({ isOpen: true }), + onClose: () => set({ isOpen: false }), +})); diff --git a/apps/www/src/hooks/use-local-storage.ts b/apps/www/src/hooks/use-local-storage.ts new file mode 100644 index 0000000..aa3984b --- /dev/null +++ b/apps/www/src/hooks/use-local-storage.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from "react"; + +const useLocalStorage = ( + key: string, + initialValue: T, +): [T, (value: T) => void] => { + const [storedValue, setStoredValue] = useState(initialValue); + + useEffect(() => { + // Retrieve from localStorage + const item = window.localStorage.getItem(key); + if (item) { + setStoredValue(JSON.parse(item)); + } + }, [key]); + + const setValue = (value: T) => { + // Save state + setStoredValue(value); + // Save to localStorage + window.localStorage.setItem(key, JSON.stringify(value)); + }; + return [storedValue, setValue]; +}; + +export default useLocalStorage; diff --git a/apps/www/src/hooks/use-lock-body.ts b/apps/www/src/hooks/use-lock-body.ts new file mode 100644 index 0000000..5b8b72c --- /dev/null +++ b/apps/www/src/hooks/use-lock-body.ts @@ -0,0 +1,12 @@ +import * as React from "react"; + +// @see https://usehooks.com/useLockBodyScroll. +export function useLockBody() { + React.useLayoutEffect((): (() => void) => { + const originalStyle: string = window.getComputedStyle( + document.body, + ).overflow; + document.body.style.overflow = "hidden"; + return () => (document.body.style.overflow = originalStyle); + }, []); +} diff --git a/apps/www/src/hooks/use-media-query.ts b/apps/www/src/hooks/use-media-query.ts new file mode 100644 index 0000000..c91e543 --- /dev/null +++ b/apps/www/src/hooks/use-media-query.ts @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; + +export default function useMediaQuery() { + const [device, setDevice] = useState<"mobile" | "tablet" | "desktop" | null>( + null, + ); + const [dimensions, setDimensions] = useState<{ + width: number; + height: number; + } | null>(null); + + useEffect(() => { + const checkDevice = () => { + if (window.matchMedia("(max-width: 640px)").matches) { + setDevice("mobile"); + } else if ( + window.matchMedia("(min-width: 641px) and (max-width: 1024px)").matches + ) { + setDevice("tablet"); + } else { + setDevice("desktop"); + } + setDimensions({ width: window.innerWidth, height: window.innerHeight }); + }; + + // Initial detection + checkDevice(); + + // Listener for windows resize + window.addEventListener("resize", checkDevice); + + // Cleanup listener + return () => { + window.removeEventListener("resize", checkDevice); + }; + }, []); + + return { + device, + width: dimensions?.width, + height: dimensions?.height, + isMobile: device === "mobile", + isTablet: device === "tablet", + isDesktop: device === "desktop", + }; +} diff --git a/apps/www/src/hooks/use-mounted.ts b/apps/www/src/hooks/use-mounted.ts new file mode 100644 index 0000000..57bb851 --- /dev/null +++ b/apps/www/src/hooks/use-mounted.ts @@ -0,0 +1,11 @@ +import * as React from "react"; + +export function useMounted() { + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + }, []); + + return mounted; +} diff --git a/apps/www/src/hooks/use-scroll.ts b/apps/www/src/hooks/use-scroll.ts new file mode 100644 index 0000000..3c8014e --- /dev/null +++ b/apps/www/src/hooks/use-scroll.ts @@ -0,0 +1,16 @@ +import { useCallback, useEffect, useState } from "react"; + +export default function useScroll(threshold: number) { + const [scrolled, setScrolled] = useState(false); + + const onScroll = useCallback(() => { + setScrolled(window.pageYOffset > threshold); + }, [threshold]); + + useEffect(() => { + window.addEventListener("scroll", onScroll); + return () => window.removeEventListener("scroll", onScroll); + }, [onScroll]); + + return scrolled; +} diff --git a/apps/www/src/hooks/use-signin-modal.ts b/apps/www/src/hooks/use-signin-modal.ts new file mode 100644 index 0000000..ab0e06a --- /dev/null +++ b/apps/www/src/hooks/use-signin-modal.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface useSigninModalStore { + isOpen: boolean; + onOpen: () => void; + onClose: () => void; +} + +export const useSigninModal = create ((set) => ({ + isOpen: false, + onOpen: () => set({ isOpen: true }), + onClose: () => set({ isOpen: false }), +})); diff --git a/apps/www/src/lib/auth.ts b/apps/www/src/lib/auth.ts new file mode 100644 index 0000000..6434574 --- /dev/null +++ b/apps/www/src/lib/auth.ts @@ -0,0 +1,126 @@ +import type { NextAuthOptions } from "next-auth"; +import sendOnboardingEmail from "@/actions/send-onboarding-email"; +import { siteConfig } from "@/config/site"; +import MagicLinkEmail from "@/emails/magic-link-email"; +import { env } from "@/env"; +import { PrismaAdapter } from "@next-auth/prisma-adapter"; +import EmailProvider from "next-auth/providers/email"; +import GoogleProvider from "next-auth/providers/google"; + +import { resend } from "./email"; +import { prisma } from "./db"; + +export const authOptions: NextAuthOptions = { + adapter: PrismaAdapter(prisma), + session: { + strategy: "jwt", + }, + pages: { + signIn: "/login", + }, + providers: [ + GoogleProvider({ + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, + }), + EmailProvider({ + sendVerificationRequest: async ({ identifier, url, provider }) => { + const user = await prisma.user.findUnique({ + where: { + email: identifier, + }, + select: { + name: true, + emailVerified: true, + }, + }); + + const userVerified = user?.emailVerified ? true : false; + const authSubject = userVerified + ? `Sign-in link for ${siteConfig.name}` + : "Activate your account"; + + try { + const result = await resend.emails.send({ + from: "Propwrite App ", + to: + process.env.NODE_ENV === "development" + ? "delivered@resend.dev" + : identifier, + subject: authSubject, + react: MagicLinkEmail({ + firstName: user?.name!, + actionUrl: url, + mailType: userVerified ? "login" : "register", + siteName: siteConfig.name, + }), + // Set this to prevent Gmail from threading emails. + // More info: https://resend.com/changelog/custom-email-headers + headers: { + "X-Entity-Ref-ID": new Date().getTime() + "", + }, + }); + + // console.log(result) + } catch (error) { + throw new Error("Failed to send verification email."); + } + }, + }), + ], + callbacks: { + async session({ token, session }) { + if (token) { + session.user.id = token.id; + session.user.name = token.name; + session.user.email = token.email; + session.user.image = token.picture; + } + + return session; + }, + async jwt({ token, user }) { + const dbUser = await prisma.user.findFirst({ + where: { + email: token.email, + }, + }); + + if (dbUser && !dbUser.onboardingEmailSent) { + // Ensure email and name are not null before sending + if (dbUser.email && dbUser.name) { + await sendOnboardingEmail(dbUser.email, dbUser.name); + + await prisma.user.update({ + where: { email: dbUser.email }, + data: { onboardingEmailSent: true }, + }); + + console.log(`Onboarding email sent to ${dbUser.email}`); + } else { + console.log( + `User email or name is null for user with email: ${token.email}`, + ); + } + } + + if (!dbUser) { + if (user) { + token.id = user.id!; + token.email = user.email!; + token.name = user.name!; + // Add other necessary token assignments + } + return token; + } + + return { + id: dbUser.id, + name: dbUser.name, + email: dbUser.email, + picture: dbUser.image, + }; + }, + }, + // debug: process.env.NODE_ENV !== "production" +}; diff --git a/apps/www/src/lib/crypto.ts b/apps/www/src/lib/crypto.ts new file mode 100644 index 0000000..f10cb1e --- /dev/null +++ b/apps/www/src/lib/crypto.ts @@ -0,0 +1,7 @@ +export function generateApiKey() { + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join( + "", + ); +} diff --git a/apps/www/src/lib/db.ts b/apps/www/src/lib/db.ts new file mode 100644 index 0000000..de55062 --- /dev/null +++ b/apps/www/src/lib/db.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ + +import { env } from "@/env"; +import { Pool, PrismaClient, PrismaNeon } from "@dingify/db"; + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; + +const pool = new Pool({ connectionString: env.DATABASE_URL }); +const adapter = new PrismaNeon(pool); + +export const prisma = + globalForPrisma.prisma || + new PrismaClient({ + adapter: adapter, + log: + env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], + }); + +if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/apps/www/src/lib/email.ts b/apps/www/src/lib/email.ts new file mode 100644 index 0000000..10be544 --- /dev/null +++ b/apps/www/src/lib/email.ts @@ -0,0 +1,4 @@ +import { env } from "@/env"; +import { Resend } from "resend"; + +export const resend = new Resend(env.RESEND_API_KEY); diff --git a/apps/www/src/lib/exceptions.ts b/apps/www/src/lib/exceptions.ts new file mode 100644 index 0000000..2bef873 --- /dev/null +++ b/apps/www/src/lib/exceptions.ts @@ -0,0 +1,5 @@ +export class RequiresProPlanError extends Error { + constructor(message = "This action requires a pro plan") { + super(message); + } +} diff --git a/apps/www/src/lib/pixel-events.tsx b/apps/www/src/lib/pixel-events.tsx new file mode 100644 index 0000000..555bf5d --- /dev/null +++ b/apps/www/src/lib/pixel-events.tsx @@ -0,0 +1,20 @@ +"use client"; + +import React, { useEffect } from "react"; +import { usePathname, useSearchParams } from "next/navigation"; + +export const FacebookPixelEvents: React.FC = () => { + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + import("react-facebook-pixel") + .then((x) => x.default) + .then((ReactPixel) => { + ReactPixel.init("1442315969659116"); //don't + ReactPixel.pageView(); + }); + }, [pathname, searchParams]); + + return null; +}; diff --git a/apps/www/src/lib/session.ts b/apps/www/src/lib/session.ts new file mode 100644 index 0000000..89c9a89 --- /dev/null +++ b/apps/www/src/lib/session.ts @@ -0,0 +1,8 @@ +import { authOptions } from "@/lib/auth"; +import { getServerSession } from "next-auth/next"; + +export async function getCurrentUser() { + const session = await getServerSession(authOptions); + + return session?.user; +} diff --git a/apps/www/src/lib/stripe.ts b/apps/www/src/lib/stripe.ts new file mode 100644 index 0000000..397a1c9 --- /dev/null +++ b/apps/www/src/lib/stripe.ts @@ -0,0 +1,7 @@ +import { env } from "@/env"; +import Stripe from "stripe"; + +export const stripe = new Stripe(env.STRIPE_API_KEY, { + apiVersion: "2023-10-16", + typescript: true, +}); diff --git a/apps/www/src/lib/subscription.ts b/apps/www/src/lib/subscription.ts new file mode 100644 index 0000000..33af0f4 --- /dev/null +++ b/apps/www/src/lib/subscription.ts @@ -0,0 +1,66 @@ +// @ts-nocheck +// TODO: Fix this when we turn strict mode on. +import type { UserSubscriptionPlan } from "@/types"; + +import { pricingData } from "@/config/subscriptions"; +import { prisma } from "@/lib/db"; +import { stripe } from "@/lib/stripe"; + +export async function getUserSubscriptionPlan( + userId: string, +): Promise { + const user = await prisma.user.findFirst({ + where: { + id: userId, + }, + select: { + stripeSubscriptionId: true, + stripeCurrentPeriodEnd: true, + stripeCustomerId: true, + stripePriceId: true, + }, + }); + + if (!user) { + throw new Error("User not found"); + } + + // Check if user is on a paid plan. + const isPaid = + user.stripePriceId && + user.stripeCurrentPeriodEnd?.getTime() + 86_400_000 > Date.now() + ? true + : false; + + // Find the pricing data corresponding to the user's plan + const userPlan = + pricingData.find((plan) => plan.stripeIds.monthly === user.stripePriceId) || + pricingData.find((plan) => plan.stripeIds.yearly === user.stripePriceId); + + const plan = isPaid && userPlan ? userPlan : pricingData[0]; + + const interval = isPaid + ? userPlan?.stripeIds.monthly === user.stripePriceId + ? "month" + : userPlan?.stripeIds.yearly === user.stripePriceId + ? "year" + : null + : null; + + let isCanceled = false; + if (isPaid && user.stripeSubscriptionId) { + const stripePlan = await stripe.subscriptions.retrieve( + user.stripeSubscriptionId, + ); + isCanceled = stripePlan.cancel_at_period_end; + } + + return { + ...plan, + ...user, + stripeCurrentPeriodEnd: user.stripeCurrentPeriodEnd?.getTime(), + isPaid, + interval, + isCanceled, + }; +} diff --git a/apps/www/src/lib/toc.ts b/apps/www/src/lib/toc.ts new file mode 100644 index 0000000..f9ba366 --- /dev/null +++ b/apps/www/src/lib/toc.ts @@ -0,0 +1,79 @@ +// @ts-nocheck +// TODO: Fix this when we turn strict mode on. + +import { toc } from "mdast-util-toc"; +import { remark } from "remark"; +import { visit } from "unist-util-visit"; + +const textTypes = ["text", "emphasis", "strong", "inlineCode"]; + +function flattenNode(node) { + const p = []; + visit(node, (node) => { + if (!textTypes.includes(node.type)) return; + p.push(node.value); + }); + return p.join(``); +} + +interface Item { + title: string; + url: string; + items?: Item[]; +} + +interface Items { + items?: Item[]; +} + +function getItems(node, current): Items { + if (!node) { + return {}; + } + + if (node.type === "paragraph") { + visit(node, (item) => { + if (item.type === "link") { + current.url = item.url; + current.title = flattenNode(node); + } + + if (item.type === "text") { + current.title = flattenNode(node); + } + }); + + return current; + } + + if (node.type === "list") { + current.items = node.children.map((i) => getItems(i, {})); + + return current; + } else if (node.type === "listItem") { + const heading = getItems(node.children[0], {}); + + if (node.children.length > 1) { + getItems(node.children[1], heading); + } + + return heading; + } + + return {}; +} + +const getToc = () => (node, file) => { + const table = toc(node); + file.data = getItems(table.map, {}); +}; + +export type TableOfContents = Items; + +export async function getTableOfContents( + content: string, +): Promise { + const result = await remark().use(getToc).process(content); + + return result.data; +} diff --git a/apps/www/src/lib/utils.ts b/apps/www/src/lib/utils.ts new file mode 100644 index 0000000..44a43f7 --- /dev/null +++ b/apps/www/src/lib/utils.ts @@ -0,0 +1,174 @@ +import type { ClassValue } from "clsx"; +import { clsx } from "clsx"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; + +import { env } from "@/env"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatDate(input: string | number): string { + const date = new Date(input); + return date.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }); +} + +export function absoluteUrl(path: string) { + return `${env.NEXT_PUBLIC_APP_URL}${path}`; +} + +// Utils from precedent.dev + +export const timeAgo = (timestamp: Date, timeOnly?: boolean): string => { + if (!timestamp) return "never"; + return `${ms(Date.now() - new Date(timestamp).getTime())}${ + timeOnly ? "" : " ago" + }`; +}; + +export async function fetcher ( + input: RequestInfo, + init?: RequestInit, +): Promise { + const res = await fetch(input, init); + + if (!res.ok) { + const json = await res.json(); + if (json.error) { + const error = new Error(json.error) as Error & { + status: number; + }; + error.status = res.status; + throw error; + } else { + throw new Error("An unexpected error occurred"); + } + } + + return res.json(); +} + +export function nFormatter(num: number, digits?: number) { + if (!num) return "0"; + const lookup = [ + { value: 1, symbol: "" }, + { value: 1e3, symbol: "K" }, + { value: 1e6, symbol: "M" }, + { value: 1e9, symbol: "G" }, + { value: 1e12, symbol: "T" }, + { value: 1e15, symbol: "P" }, + { value: 1e18, symbol: "E" }, + ]; + const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; + const item = lookup + .slice() + .reverse() + .find(function (item) { + return num >= item.value; + }); + return item + ? (num / item.value).toFixed(digits || 1).replace(rx, "$1") + item.symbol + : "0"; +} + +export function capitalize(str: string) { + if (!str || typeof str !== "string") return str; + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export const truncate = (str: string, length: number) => { + if (!str || str.length <= length) return str; + return `${str.slice(0, length)}...`; +}; + +// utils/greeting.js +export function getGreeting() { + const now = new Date(); + const hours = now.getHours(); + const day = now.getDay(); // Sunday = 0, Monday = 1, ..., Saturday = 6 + + // Determine time-based greeting + if (day === 6 || day === 0) { + return "this weekend"; + } else if (hours >= 6 && hours < 12) { + return "morning"; + } else if (hours >= 12 && hours < 21) { + return "afternoon"; + } else if (hours >= 21 || hours < 6) { + return "night"; + } + + // Fallback + return "today"; +} + +export async function fetchGithubData() { + try { + const githubInfoResponse = await fetch( + "https://api.github.com/repos/Codehagen/Dingify", + ); + if (!githubInfoResponse.ok) throw new Error("Failed to fetch GitHub info"); + const data = await githubInfoResponse.json(); + + const prsResponse = await fetch( + "https://api.github.com/search/issues?q=repo:Codehagen/Dingify+type:pr+is:merged", + ); + if (!prsResponse.ok) throw new Error("Failed to fetch PRs info"); + const totalPR = await prsResponse.json(); + + return { + stargazers_count: data.stargazers_count, + open_issues: data.open_issues, + total_count: totalPR.total_count, + forks: data.forks, + }; + } catch (error) { + console.error("Error fetching GitHub data:", error); + throw error; + } +} + +export async function sendDiscordNotification(webhookUrl, message) { + try { + const response = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: message, + }), + }); + + if (!response.ok) { + throw new Error("Failed to send Discord notification"); + } + } catch (error) { + console.error("Error sending notification to Discord:", error); + } +} + +export async function sendSlackNotification(webhookUrl, message) { + try { + const response = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: message, + }), + }); + + if (!response.ok) { + throw new Error("Failed to send Slack notification"); + } + } catch (error) { + console.error("Error sending notification to Slack:", error); + } +} diff --git a/apps/www/src/lib/validations/auth.ts b/apps/www/src/lib/validations/auth.ts new file mode 100644 index 0000000..dab2901 --- /dev/null +++ b/apps/www/src/lib/validations/auth.ts @@ -0,0 +1,5 @@ +import * as z from "zod"; + +export const userAuthSchema = z.object({ + email: z.string().email(), +}); diff --git a/apps/www/src/lib/validations/og.ts b/apps/www/src/lib/validations/og.ts new file mode 100644 index 0000000..a26f14c --- /dev/null +++ b/apps/www/src/lib/validations/og.ts @@ -0,0 +1,7 @@ +import * as z from "zod"; + +export const ogImageSchema = z.object({ + heading: z.string(), + type: z.string(), + mode: z.enum(["light", "dark"]).default("dark"), +}); diff --git a/apps/www/src/lib/validations/user.ts b/apps/www/src/lib/validations/user.ts new file mode 100644 index 0000000..3553cd5 --- /dev/null +++ b/apps/www/src/lib/validations/user.ts @@ -0,0 +1,5 @@ +import * as z from "zod"; + +export const userNameSchema = z.object({ + name: z.string().min(3).max(32), +}); diff --git a/apps/www/src/middleware.ts b/apps/www/src/middleware.ts new file mode 100644 index 0000000..03ab77c --- /dev/null +++ b/apps/www/src/middleware.ts @@ -0,0 +1,46 @@ +import { NextResponse } from "next/server"; +import { getToken } from "next-auth/jwt"; +import { withAuth } from "next-auth/middleware"; + +export default withAuth( + async function middleware(req) { + const token = await getToken({ req }); + const isAuth = !!token; + const isAuthPage = + req.nextUrl.pathname.startsWith("/login") || + req.nextUrl.pathname.startsWith("/register"); + + if (isAuthPage) { + if (isAuth) { + return NextResponse.redirect(new URL("/dashboard", req.url)); + } + + return null; + } + + if (!isAuth) { + let from = req.nextUrl.pathname; + if (req.nextUrl.search) { + from += req.nextUrl.search; + } + + return NextResponse.redirect( + new URL(`/login?from=${encodeURIComponent(from)}`, req.url), + ); + } + }, + { + callbacks: { + async authorized() { + // This is a work-around for handling redirect on auth pages. + // We return true here so that the middleware function above + // is always called. + return true; + }, + }, + }, +); + +export const config = { + matcher: ["/dashboard/:path*", "/login", "/register"], +}; diff --git a/apps/www/src/styles/globals.css b/apps/www/src/styles/globals.css new file mode 100644 index 0000000..a26eb14 --- /dev/null +++ b/apps/www/src/styles/globals.css @@ -0,0 +1,74 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0.5rem; + + /* Custom properties */ + --navigation-height: 3.5rem; + --color-one: #ffbd7a; + --color-two: #fe8bbb; + --color-three: #9e7aff; + + /* + --color-one: #37ecba; + --color-two: #72afd3; + --color-three: #ff2e63; + */ + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: + "rlig" 1, + "calt" 1; + } +} diff --git a/apps/www/src/styles/mdx.css b/apps/www/src/styles/mdx.css new file mode 100644 index 0000000..88be5cf --- /dev/null +++ b/apps/www/src/styles/mdx.css @@ -0,0 +1,32 @@ +[data-rehype-pretty-code-fragment] code { + @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0 text-sm text-black; + counter-reset: line; + box-decoration-break: clone; +} +[data-rehype-pretty-code-fragment] .line { + @apply px-4 py-1; +} +[data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before { + counter-increment: line; + content: counter(line); + display: inline-block; + width: 1rem; + margin-right: 1rem; + text-align: right; + color: gray; +} +[data-rehype-pretty-code-fragment] .line--highlighted { + @apply bg-slate-300 bg-opacity-10; +} +[data-rehype-pretty-code-fragment] .line-highlighted span { + @apply relative; +} +[data-rehype-pretty-code-fragment] .word--highlighted { + @apply rounded-md bg-slate-300 bg-opacity-10 p-1; +} +[data-rehype-pretty-code-title] { + @apply mt-4 px-4 py-2 text-sm font-medium; +} +[data-rehype-pretty-code-title] + pre { + @apply mt-0; +} diff --git a/apps/www/src/types/index.d.ts b/apps/www/src/types/index.d.ts new file mode 100644 index 0000000..6077c60 --- /dev/null +++ b/apps/www/src/types/index.d.ts @@ -0,0 +1,81 @@ +import type { Icon } from "lucide-react"; +import { Icons } from "@/components/shared/icons"; +import { User } from "@prisma/client"; + +export type NavItem = { + title: string; + href: string; + disabled?: boolean; +}; + +export type MainNavItem = NavItem; + +export type SidebarNavItem = { + title: string; + disabled?: boolean; + external?: boolean; + icon?: keyof typeof Icons; +} & ( + | { + href: string; + items?: never; + } + | { + href?: string; + items: NavLink[]; + } +); + +export type SiteConfig = { + name: string; + description: string; + url: string; + ogImage: string; + mailSupport: string; + links: { + twitter: string; + github: string; + }; +}; + +export type DocsConfig = { + mainNav: MainNavItem[]; + sidebarNav: SidebarNavItem[]; +}; + +export type MarketingConfig = { + mainNav: MainNavItem[]; +}; + +export type DashboardConfig = { + mainNav: MainNavItem[]; + sidebarNav: SidebarNavItem[]; +}; + +export type PropertyConfig = { + mainNav: MainNavItem[]; + sidebarNav: SidebarNavItem[]; +}; + +export type SubscriptionPlan = { + title: string; + description: string; + benefits: string[]; + limitations: string[]; + prices: { + monthly: number; + yearly: number; + }; + stripeIds: { + monthly: string | null; + yearly: string | null; + }; +}; + +export type UserSubscriptionPlan = SubscriptionPlan & + Pick & { + stripeCurrentPeriodEnd: number; + isPaid: boolean; + interval: "month" | "year" | null; + isCanceled?: boolean; + }; diff --git a/apps/www/src/types/next-auth.d.ts b/apps/www/src/types/next-auth.d.ts new file mode 100644 index 0000000..343ea77 --- /dev/null +++ b/apps/www/src/types/next-auth.d.ts @@ -0,0 +1,18 @@ +import { User } from "next-auth"; +import { JWT } from "next-auth/jwt"; + +type UserId = string; + +declare module "next-auth/jwt" { + interface JWT { + id: UserId; + } +} + +declare module "next-auth" { + interface Session { + user: User & { + id: UserId; + }; + } +} diff --git a/apps/www/src/types/types.ts b/apps/www/src/types/types.ts new file mode 100644 index 0000000..71741ae --- /dev/null +++ b/apps/www/src/types/types.ts @@ -0,0 +1,54 @@ +// types.ts +import { z } from "zod"; + +// Define TypeScript types +export type Event = { + id: string; + name: string; + channelId: string; + userId: string; + icon: string; + notify: boolean; + tags: Record ; + createdAt: string; +}; + +export type ChannelDetails = { + id: string; + name: string; + projectId: string; + createdAt: string; + project: { + id: string; + name: string; + userId: string; + createdAt: string; + }; + events: Event[]; +}; + +// Define Zod schemas +export const eventSchema = z.object({ + id: z.string(), + name: z.string(), + channelId: z.string(), + userId: z.string(), + icon: z.string(), + notify: z.boolean(), + tags: z.record(z.unknown()), + createdAt: z.string(), +}); + +export const channelDetailsSchema = z.object({ + id: z.string(), + name: z.string(), + projectId: z.string(), + createdAt: z.string(), + project: z.object({ + id: z.string(), + name: z.string(), + userId: z.string(), + createdAt: z.string(), + }), + events: z.array(eventSchema), +}); diff --git a/apps/www/tailwind.config.ts b/apps/www/tailwind.config.ts new file mode 100644 index 0000000..0814c4e --- /dev/null +++ b/apps/www/tailwind.config.ts @@ -0,0 +1,138 @@ +import type { Config } from "tailwindcss"; +import { fontFamily } from "tailwindcss/defaultTheme"; + +import baseConfig from "@dingify/tailwind-config"; + +const config: Config = { + // Append the path to the UI package to the content array + content: [ + ...baseConfig.content, + "../../packages/ui/src/**/*.{ts,tsx}", + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + presets: [baseConfig], + darkMode: ["class"], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + fontFamily: { + sans: ["var(--font-sans)", ...fontFamily.sans], + }, + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + "border-beam": { + "100%": { + "offset-distance": "100%", + }, + }, + "image-glow": { + "0%": { + opacity: "0", + "animation-timing-function": "cubic-bezier(0.74, 0.25, 0.76, 1)", + }, + "10%": { + opacity: "0.7", + "animation-timing-function": "cubic-bezier(0.12, 0.01, 0.08, 0.99)", + }, + "100%": { + opacity: "0.4", + }, + }, + "fade-in": { + from: { opacity: "0", transform: "translateY(-10px)" }, + to: { opacity: "1", transform: "none" }, + }, + "fade-up": { + from: { opacity: "0", transform: "translateY(20px)" }, + to: { opacity: "1", transform: "none" }, + }, + shimmer: { + "0%, 90%, 100%": { + "background-position": "calc(-100% - var(--shimmer-width)) 0", + }, + "30%, 60%": { + "background-position": "calc(100% + var(--shimmer-width)) 0", + }, + }, + marquee: { + from: { transform: "translateX(0)" }, + to: { transform: "translateX(calc(-100% - var(--gap)))" }, + }, + "marquee-vertical": { + from: { transform: "translateY(0)" }, + to: { transform: "translateY(calc(-100% - var(--gap)))" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + "border-beam": "border-beam calc(var(--duration)*1s) infinite linear", + "image-glow": "image-glow 4100ms 600ms ease-out forwards", + "fade-in": "fade-in 1000ms var(--animation-delay, 0ms) ease forwards", + "fade-up": "fade-up 1000ms var(--animation-delay, 0ms) ease forwards", + shimmer: "shimmer 8s infinite", + marquee: "marquee var(--duration) infinite linear", + "marquee-vertical": "marquee-vertical var(--duration) linear infinite", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; + +export default config; diff --git a/apps/www/tsconfig.json b/apps/www/tsconfig.json new file mode 100644 index 0000000..56fb116 --- /dev/null +++ b/apps/www/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "@dingify/tsconfig/base.json", + "compilerOptions": { + "lib": ["es2022", "dom", "dom.iterable"], + "jsx": "preserve", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "contentlayer/generated": ["./.contentlayer/generated"] + }, + "plugins": [{ "name": "next" }], + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", + "module": "esnext", + "noImplicitAny": false, // TODO: Resolve errors when setting this to 'true' + "useUnknownInCatchVariables": false // TODO: Resolve errors when setting this to 'true' + }, + "include": ["src", ".next/types/**/*.ts"], + "exclude": ["node_modules", ".contentlayer/generated"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d85e4c8 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "dingify", + "version": "0.1.0", + "private": true, + "author": { + "name": "Christer", + "url": "https://twitter.com/Codehagen" + }, + "engines": { + "node": "20.10.0" + }, + "packageManager": "pnpm@8.15.5", + "prettier": "@dingify/prettier-config", + "scripts": { + "build": "turbo build", + "clean": "git clean -xdf node_modules", + "clean:workspaces": "turbo clean", + "db:generate": "pnpm -F db db:generate", + "db:migrate:dev": "pnpm -F db db:migrate:dev", + "db:studio": "pnpm -F db db:studio", + "dev": "turbo dev --parallel", + "format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache", + "format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache", + "lint": "turbo lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache", + "lint:fix": "turbo lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache", + "lint:ws": "pnpm dlx sherif@latest", + "postinstall": "pnpm lint:ws", + "typecheck": "turbo typecheck" + }, + "devDependencies": { + "@dingify/prettier-config": "workspace:*", + "husky": "^8.0.3", + "prettier": "^3.2.5", + "turbo": "^1.13.2", + "typescript": "^5.4.5", + "@commitlint/cli": "^18.2.0", + "@commitlint/config-conventional": "^18.1.0" + } +} diff --git a/packages/db/eslint.config.js b/packages/db/eslint.config.js new file mode 100644 index 0000000..2f297a6 --- /dev/null +++ b/packages/db/eslint.config.js @@ -0,0 +1,9 @@ +import baseConfig from "@dingify/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: ["dist/**"], + }, + ...baseConfig, +]; diff --git a/packages/db/index.ts b/packages/db/index.ts new file mode 100644 index 0000000..fb262f5 --- /dev/null +++ b/packages/db/index.ts @@ -0,0 +1,3 @@ +export * from "@prisma/client"; +export * from "@prisma/adapter-neon"; +export * from "@neondatabase/serverless"; diff --git a/packages/db/package.json b/packages/db/package.json new file mode 100644 index 0000000..d3cefbe --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,45 @@ +{ + "name": "@dingify/db", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./index.ts", + "default": "./index.ts" + } + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf .turbo node_modules", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint .", + "typecheck": "tsc --noEmit --emitDeclarationOnly false", + "with-env": "dotenv -e ../../.env --", + "db:generate": "pnpm with-env prisma generate", + "db:push": "pnpm with-env prisma db push --skip-generate", + "db:migrate:dev": "pnpm with-env prisma migrate dev", + "db:migrate:reset": "pnpm with-env prisma migrate reset", + "db:migrate:deploy": "pnpm with-env prisma migrate deploy", + "db:studio": "pnpm with-env prisma studio", + "db:force": "pnpm with-env prisma db push --force-reset", + "postinstall": "pnpm db:generate" + }, + "dependencies": { + "@neondatabase/serverless": "^0.9.3", + "@prisma/adapter-neon": "^5.13.0", + "@prisma/client": "^5.12.1", + "@t3-oss/env-core": "^0.9.2" + }, + "devDependencies": { + "@dingify/eslint-config": "workspace:*", + "@dingify/prettier-config": "workspace:*", + "@dingify/tsconfig": "workspace:*", + "dotenv-cli": "^7.4.1", + "eslint": "^9.0.0", + "prettier": "^3.2.5", + "prisma": "^5.12.1", + "typescript": "^5.4.5" + }, + "prettier": "@dingify/prettier-config" +} diff --git a/packages/db/prisma/migrations/0_init/migration.sql b/packages/db/prisma/migrations/0_init/migration.sql new file mode 100644 index 0000000..214e409 --- /dev/null +++ b/packages/db/prisma/migrations/0_init/migration.sql @@ -0,0 +1,64 @@ +-- CreateTable +CREATE TABLE `accounts` ( + `id` VARCHAR(191) NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `type` VARCHAR(191) NOT NULL, + `provider` VARCHAR(191) NOT NULL, + `providerAccountId` VARCHAR(191) NOT NULL, + `refresh_token` TEXT NULL, + `access_token` TEXT NULL, + `expires_at` INTEGER NULL, + `token_type` VARCHAR(191) NULL, + `scope` VARCHAR(191) NULL, + `id_token` TEXT NULL, + `session_state` VARCHAR(191) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `accounts_userId_idx`(`userId`), + UNIQUE INDEX `accounts_provider_providerAccountId_key`(`provider`, `providerAccountId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sessions` ( + `id` VARCHAR(191) NOT NULL, + `sessionToken` VARCHAR(191) NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `expires` DATETIME(3) NOT NULL, + + UNIQUE INDEX `sessions_sessionToken_key`(`sessionToken`), + INDEX `sessions_userId_idx`(`userId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `users` ( + `id` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NULL, + `email` VARCHAR(191) NULL, + `emailVerified` DATETIME(3) NULL, + `image` VARCHAR(191) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `stripe_customer_id` VARCHAR(191) NULL, + `stripe_subscription_id` VARCHAR(191) NULL, + `stripe_price_id` VARCHAR(191) NULL, + `stripe_current_period_end` DATETIME(3) NULL, + + UNIQUE INDEX `users_email_key`(`email`), + UNIQUE INDEX `users_stripe_customer_id_key`(`stripe_customer_id`), + UNIQUE INDEX `users_stripe_subscription_id_key`(`stripe_subscription_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `verification_tokens` ( + `identifier` VARCHAR(191) NOT NULL, + `token` VARCHAR(191) NOT NULL, + `expires` DATETIME(3) NOT NULL, + + UNIQUE INDEX `verification_tokens_token_key`(`token`), + UNIQUE INDEX `verification_tokens_identifier_token_key`(`identifier`, `token`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma new file mode 100644 index 0000000..59a6e17 --- /dev/null +++ b/packages/db/prisma/schema.prisma @@ -0,0 +1,171 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DATABASE_URL_UNPOOLED") +} + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @map(name: "updated_at") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([userId]) + @@map(name: "accounts") +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@map(name: "sessions") +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + apiKey String? @unique + plan String @default("basic") + credits Int @default(3) + image String? + language String? @default("english") + onboardingEmailSent Boolean @default(false) + + accounts Account[] + sessions Session[] + projects Project[] + notificationSettings NotificationSetting[] + + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @map(name: "updated_at") + + stripeCustomerId String? @unique @map(name: "stripe_customer_id") + stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id") + stripePriceId String? @map(name: "stripe_price_id") + stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end") + + @@map(name: "users") +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) + @@map(name: "verification_tokens") +} + +model Project { + id String @id @default(cuid()) + name String + userId String + user User @relation(fields: [userId], references: [id]) + + channels Channel[] + metrics Metrics[] + customers Customer[] + + createdAt DateTime @default(now()) + + @@map(name: "projects") +} + +model Channel { + id String @id @default(cuid()) + name String + projectId String + project Project @relation(fields: [projectId], references: [id]) + events Event[] + createdAt DateTime @default(now()) + + @@unique([projectId, name]) + @@map(name: "channels") +} + +model Event { + id String @id @default(cuid()) + name String + channelId String + channel Channel @relation(fields: [channelId], references: [id]) + userId String + customerId String? + customer Customer? @relation(fields: [customerId], references: [id]) + icon String + notify Boolean + tags Json + createdAt DateTime @default(now()) + + @@map(name: "events") +} + +model Metrics { + id String @id @default(cuid()) + projectId String @unique + logsUsed Int @default(0) + logsLimit Int @default(1000) + channelsUsed Int @default(1) + channelsLimit Int @default(3) + seatsUsed Int @default(1) + projectsUsed Int @default(1) + project Project @relation(fields: [projectId], references: [id]) + + @@map(name: "metrics") +} + +model Customer { + id String @id @default(cuid()) + projectId String + userId String + name String? + email String? + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + events Event[] + + @@unique([userId, projectId]) + @@map(name: "customers") +} + +model NotificationSetting { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id]) + type NotificationType @default(DISCORD) + details Json // Details like webhook URL, mobile number, etc. + enabled Boolean @default(true) + createdAt DateTime @default(now()) + + @@map(name: "notification_settings") +} + +enum NotificationType { + DISCORD + SLACK + MOBILE +} + +// if we need full reset : npx prisma db push --force-reset diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json new file mode 100644 index 0000000..344037e --- /dev/null +++ b/packages/db/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@dingify/tsconfig/internal-package.json", + "compilerOptions": { + "outDir": "dist", + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["."], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui/eslint.config.js b/packages/ui/eslint.config.js new file mode 100644 index 0000000..a72c8b8 --- /dev/null +++ b/packages/ui/eslint.config.js @@ -0,0 +1,13 @@ +import baseConfig from "@dingify/eslint-config/base"; +import reactConfig from "@dingify/eslint-config/react"; + +/** @type {import('typescript-eslint').Config} */ +const config = [ + { + ignores: [], + }, + ...baseConfig, + ...reactConfig, +]; + +export default config; diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..0e87379 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,68 @@ +{ + "name": "@dingify/ui", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./src/index.ts", + "./*": [ + "./src/*.tsx", + "./src/*.ts" + ] + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf .turbo node_modules", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint ./src", + "typecheck": "tsc --noEmit --emitDeclarationOnly false" + }, + "dependencies": { + "@radix-ui/react-accessible-icon": "^1.0.3", + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-aspect-ratio": "^1.0.3", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-context-menu": "^2.1.5", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-hover-card": "^1.0.7", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-menubar": "^1.0.4", + "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-toggle": "^1.0.3", + "@radix-ui/react-toggle-group": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "cmdk": "^0.2.0", + "lucide-react": "^0.292.0", + "next-themes": "^0.2.1", + "react-day-picker": "^8.9.1", + "react-hook-form": "^7.47.0", + "sonner": "^1.4.41", + "tailwind-merge": "^2.0.0" + }, + "devDependencies": { + "@dingify/eslint-config": "workspace:*", + "@dingify/prettier-config": "workspace:*", + "@dingify/tailwind-config": "workspace:*", + "@dingify/tsconfig": "workspace:*", + "react": "18.2.0" + }, + "prettier": "@dingify/prettier-config" +} diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx new file mode 100644 index 0000000..7c96915 --- /dev/null +++ b/packages/ui/src/components/accordion.tsx @@ -0,0 +1,60 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "../utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRefsvg]:rotate-180", + className, + )} + {...props} + > + {children} + ++ , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + +)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/packages/ui/src/components/alert-dialog.tsx b/packages/ui/src/components/alert-dialog.tsx new file mode 100644 index 0000000..75d1f7a --- /dev/null +++ b/packages/ui/src/components/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client"; + +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "../utils"; +import { buttonVariants } from "./button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef{children}+, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes+ + ) => ( + +); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes ) => ( + +); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/packages/ui/src/components/alert.tsx b/packages/ui/src/components/alert.tsx new file mode 100644 index 0000000..05d9fba --- /dev/null +++ b/packages/ui/src/components/alert.tsx @@ -0,0 +1,60 @@ +import type { VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { cva } from "class-variance-authority"; + +import { cn } from "../utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( + +)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/packages/ui/src/components/aspect-ratio.tsx b/packages/ui/src/components/aspect-ratio.tsx new file mode 100644 index 0000000..359bc94 --- /dev/null +++ b/packages/ui/src/components/aspect-ratio.tsx @@ -0,0 +1,7 @@ +"use client"; + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +const AspectRatio = AspectRatioPrimitive.Root; + +export { AspectRatio }; diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx new file mode 100644 index 0000000..f05f8e8 --- /dev/null +++ b/packages/ui/src/components/avatar.tsx @@ -0,0 +1,50 @@ +"use client"; + +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "../utils"; + +const Avatar = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/packages/ui/src/components/badge.tsx b/packages/ui/src/components/badge.tsx new file mode 100644 index 0000000..73af153 --- /dev/null +++ b/packages/ui/src/components/badge.tsx @@ -0,0 +1,37 @@ +import type { VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { cva } from "class-variance-authority"; + +import { cn } from "../utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export interface BadgeProps + extends React.HTMLAttributes , + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx new file mode 100644 index 0000000..be80ba0 --- /dev/null +++ b/packages/ui/src/components/button.tsx @@ -0,0 +1,53 @@ +import type { VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { cva } from "class-variance-authority"; + +import { cn } from "../utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm 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", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input 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 , + VariantProps {} + +const Button = React.forwardRef ( + ({ className, variant, size, ...props }, ref) => { + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/packages/ui/src/components/calendar.tsx b/packages/ui/src/components/calendar.tsx new file mode 100644 index 0000000..b7ab417 --- /dev/null +++ b/packages/ui/src/components/calendar.tsx @@ -0,0 +1,64 @@ +"use client"; + +import * as React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { DayPicker } from "react-day-picker"; + +import { cn } from "../utils"; +import { buttonVariants } from "./button"; + +export type CalendarProps = React.ComponentProps ; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: () => , + }} + {...props} + /> + ); +} +Calendar.displayName = "Calendar"; + +export { Calendar }; diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx new file mode 100644 index 0000000..4b83566 --- /dev/null +++ b/packages/ui/src/components/card.tsx @@ -0,0 +1,86 @@ +import * as React from "react"; + +import { cn } from "../utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/packages/ui/src/components/checkbox.tsx b/packages/ui/src/components/checkbox.tsx new file mode 100644 index 0000000..34a8cc0 --- /dev/null +++ b/packages/ui/src/components/checkbox.tsx @@ -0,0 +1,30 @@ +"use client"; + +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "../utils"; + +const Checkbox = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/packages/ui/src/components/collapsible.tsx b/packages/ui/src/components/collapsible.tsx new file mode 100644 index 0000000..cb003d1 --- /dev/null +++ b/packages/ui/src/components/collapsible.tsx @@ -0,0 +1,11 @@ +"use client"; + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/packages/ui/src/components/command.tsx b/packages/ui/src/components/command.tsx new file mode 100644 index 0000000..f8eb175 --- /dev/null +++ b/packages/ui/src/components/command.tsx @@ -0,0 +1,155 @@ +"use client"; + +import type { DialogProps } from "@radix-ui/react-dialog"; +import * as React from "react"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { cn } from "../utils"; +import { Dialog, DialogContent } from "./dialog"; + +const Command = React.forwardRef< + React.ElementRef+ ++ , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +type CommandDialogProps = DialogProps; + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + ++)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef+ + , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes ) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/packages/ui/src/components/context-menu.tsx b/packages/ui/src/components/context-menu.tsx new file mode 100644 index 0000000..e56a543 --- /dev/null +++ b/packages/ui/src/components/context-menu.tsx @@ -0,0 +1,200 @@ +"use client"; + +import * as React from "react"; +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "../utils"; + +const ContextMenu = ContextMenuPrimitive.Root; + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; + +const ContextMenuGroup = ContextMenuPrimitive.Group; + +const ContextMenuPortal = ContextMenuPrimitive.Portal; + +const ContextMenuSub = ContextMenuPrimitive.Sub; + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + +)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef+ , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; + +const ContextMenuContent = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; + +const ContextMenuItem = React.forwardRef< + React.ElementRef+ , + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + +)); +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName; + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef+ + + {children} ++ , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; + +const ContextMenuLabel = React.forwardRef< + React.ElementRef+ + + {children} ++ , + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes ) => { + return ( + + ); +}; +ContextMenuShortcut.displayName = "ContextMenuShortcut"; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx new file mode 100644 index 0000000..ef38c3f --- /dev/null +++ b/packages/ui/src/components/dialog.tsx @@ -0,0 +1,122 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "../utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes+ + {children} + ++ ++ Close + ) => ( + +); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes ) => ( + +); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx new file mode 100644 index 0000000..e1ec395 --- /dev/null +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client"; + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "../utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef+ , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef+ , + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef+ + + {children} ++ , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef+ + + {children} ++ , + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes ) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/packages/ui/src/components/form.tsx b/packages/ui/src/components/form.tsx new file mode 100644 index 0000000..db53492 --- /dev/null +++ b/packages/ui/src/components/form.tsx @@ -0,0 +1,172 @@ +import type * as LabelPrimitive from "@radix-ui/react-label"; +import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form"; +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { Controller, FormProvider, useFormContext } from "react-hook-form"; + +import { cn } from "../utils"; +import { Label } from "./label"; + +const Form = FormProvider; + +interface FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath , +> { + name: TName; +} + +const FormFieldContext = React.createContext ( + {} as FormFieldContextValue +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath , +>({ + ...props +}: ControllerProps ) => { + return ( + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!fieldContext) { + throw new Error("useFormField should be used within+ "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +interface FormItemContextValue { + id: string; +} + +const FormItemContext = React.createContext ( + {} as FormItemContextValue +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + + + + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( + + ); +}); +FormLabel.displayName = "FormLabel"; + +const FormControl = React.forwardRef< + React.ElementRef , + React.ComponentPropsWithoutRef +>(({ ...props }, ref) => { + const { error, formItemId, formDescriptionId, formMessageId } = + useFormField(); + + return ( + + ); +}); +FormControl.displayName = "FormControl"; + +const FormDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { formDescriptionId } = useFormField(); + + return ( + + ); +}); +FormDescription.displayName = "FormDescription"; + +const FormMessage = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => { + const { error, formMessageId } = useFormField(); + const body = error ? String(error.message) : children; + + if (!body) { + return null; + } + + return ( +