Skip to content

Commit

Permalink
feat: add with-novel example (#800)
Browse files Browse the repository at this point in the history
This adds an example using [steventey/novel](https://github.com/steven-tey/novel).

> [!NOTE]
> This example was part of #746 but might as well pull it out to it's own thing

For convenience, it adds some additional dx helpers exported from `uploadthing/client`:

- `generateReactHelpers.getRouteConfig(endpoint: keyof Router): ExpandedRouteConfig`: helper to extract the route config outside of react context
- `isValidFileSize(file: File, config: ExpandedRouteConfig): boolean`: validate a file is within the accepted range
`isValidFileType(file: File, config: ExpandedRouteConfig): boolean`: validate a file has an accepted type

https://github.com/pingdotgg/uploadthing/blob/0ad4378d38a686615840dc1136d25b7f6ddaef08/examples/with-novel/uploadthing/novel-plugin.ts#L50-L61

https://github.com/pingdotgg/uploadthing/assets/51714798/524a4f4d-16ac-45b8-b17f-269dee549591
  • Loading branch information
juliusmarminge committed May 16, 2024
1 parent 5d7351f commit 8aa19e4
Show file tree
Hide file tree
Showing 33 changed files with 3,703 additions and 111 deletions.
8 changes: 8 additions & 0 deletions .changeset/strong-suits-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"uploadthing": minor
"@uploadthing/react": minor
---

feat: add `generateReactHelpers.getRouteConfig`, `isValidFileSize` and `isValidFileType` helpers

💡 See https://github.com/pingdotgg/uploadthing/blob/main/examples/with-novel/uploadthing/novel-plugin.ts#L50-L61 for a live example utilizing these helpers.
3 changes: 3 additions & 0 deletions examples/with-novel/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Go to https://uploadthing.com/dashboard to get your API secret
UPLOADTHING_SECRET='sk_live_xxx'
UPLOADTHING_APP_ID='xxx'
27 changes: 27 additions & 0 deletions examples/with-novel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Integrating UploadThing with Novel

<a href="https://stackblitz.com/github/pingdotgg/uploadthing/tree/main/examples/with-novel">
<img height="64" src="https://github.com/pingdotgg/uploadthing/assets/51714798/45907a4e-aa64-401a-afb3-b6c6df6eb71f" />
</a>

This is a stripped down version of the Novel Web app. See the original full
source code at: https://github.com/steven-tey/novel/tree/main/apps/web

For the UploadThing specific code in this example, see
[uploadthing/novel-plugin.ts](./uploadthing/novel-plugin.ts).

## QuickStart

1. Grab an API key from the UploadThing dashboard:
https://uploadthing.com/dashboard
2. `cp .env.example .env` and paste in your API key in the newly created `.env`
file
3. `pnpm i && pnpm dev`
4. Use the editor and upload files!

## Further reference

Check out the docs at:

- https://docs.uploadthing.com/getting-started/appdir
- https://novel.sh/docs
Binary file not shown.
47 changes: 47 additions & 0 deletions examples/with-novel/app/_styles/fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Crimson_Text, Inconsolata, Inter } from "next/font/google";
import localFont from "next/font/local";

export const cal = localFont({
src: "./CalSans-SemiBold.otf",
variable: "--font-title",
});

export const crimsonBold = Crimson_Text({
weight: "700",
variable: "--font-title",
subsets: ["latin"],
});

export const inter = Inter({
variable: "--font-default",
subsets: ["latin"],
});

export const inconsolataBold = Inconsolata({
weight: "700",
variable: "--font-title",
subsets: ["latin"],
});

export const crimson = Crimson_Text({
weight: "400",
variable: "--font-default",
subsets: ["latin"],
});

export const inconsolata = Inconsolata({
variable: "--font-default",
subsets: ["latin"],
});

export const titleFontMapper = {
Default: cal.variable,
Serif: crimsonBold.variable,
Mono: inconsolataBold.variable,
};

export const defaultFontMapper = {
Default: inter.variable,
Serif: crimson.variable,
Mono: inconsolata.variable,
};
96 changes: 96 additions & 0 deletions examples/with-novel/app/_styles/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;

--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;

--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;

--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;

--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;

--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;

--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;

--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;

--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;

--radius: 0.5rem;

--novel-highlight-default: #ffffff;
--novel-highlight-purple: #f6f3f8;
--novel-highlight-red: #fdebeb;
--novel-highlight-yellow: #fbf4a2;
--novel-highlight-blue: #c1ecf9;
--novel-highlight-green: #acf79f;
--novel-highlight-orange: #faebdd;
--novel-highlight-pink: #faf1f5;
--novel-highlight-gray: #f1f1ef;
}

.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;

--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;

--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;

--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;

--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;

--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;

--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;

--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;

--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;

--novel-highlight-default: #000000;
--novel-highlight-purple: #3f2c4b;
--novel-highlight-red: #5c1a1a;
--novel-highlight-yellow: #5c4b1a;
--novel-highlight-blue: #1a3d5c;
--novel-highlight-green: #1a5c20;
--novel-highlight-orange: #5c3a1a;
--novel-highlight-pink: #5c1a3a;
--novel-highlight-gray: #3a3a3a;
}
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
170 changes: 170 additions & 0 deletions examples/with-novel/app/_styles/prosemirror.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
.ProseMirror {
@apply p-12 px-8 sm:px-12;
}

.ProseMirror .is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: hsl(var(--muted-foreground));
pointer-events: none;
height: 0;
}
.ProseMirror .is-empty::before {
content: attr(data-placeholder);
float: left;
color: hsl(var(--muted-foreground));
pointer-events: none;
height: 0;
}

/* Custom image styles */

.ProseMirror img {
transition: filter 0.1s ease-in-out;

&:hover {
cursor: pointer;
filter: brightness(90%);
}

&.ProseMirror-selectednode {
outline: 3px solid #5abbf7;
filter: brightness(90%);
}
}

.img-placeholder {
position: relative;

&:before {
content: "";
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 36px;
height: 36px;
border-radius: 50%;
border: 3px solid var(--novel-stone-200);
border-top-color: var(--novel-stone-800);
animation: spinning 0.6s linear infinite;
}
}

@keyframes spinning {
to {
transform: rotate(360deg);
}
}

/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */

ul[data-type="taskList"] li > label {
margin-right: 0.2rem;
user-select: none;
}

@media screen and (max-width: 768px) {
ul[data-type="taskList"] li > label {
margin-right: 0.5rem;
}
}

ul[data-type="taskList"] li > label input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
background-color: hsl(var(--background));
margin: 0;
cursor: pointer;
width: 1.2em;
height: 1.2em;
position: relative;
top: 5px;
border: 2px solid hsl(var(--border));
margin-right: 0.3rem;
display: grid;
place-content: center;

&:hover {
background-color: hsl(var(--accent));
}

&:active {
background-color: hsl(var(--accent));
}

&::before {
content: "";
width: 0.65em;
height: 0.65em;
transform: scale(0);
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em;
transform-origin: center;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}

&:checked::before {
transform: scale(1);
}
}

ul[data-type="taskList"] li[data-checked="true"] > div > p {
color: var(--muted-foreground);
text-decoration: line-through;
text-decoration-thickness: 2px;
}

/* Overwrite tippy-box original max-width */

.tippy-box {
max-width: 400px !important;
}

.ProseMirror:not(.dragging) .ProseMirror-selectednode {
outline: none !important;
background-color: var(--novel-highlight-blue);
transition: background-color 0.2s;
box-shadow: none;
}

.drag-handle {
position: fixed;
opacity: 1;
transition: opacity ease-in 0.2s;
border-radius: 0.25rem;

background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E");
background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem);
background-repeat: no-repeat;
background-position: center;
width: 1.2rem;
height: 1.5rem;
z-index: 50;
cursor: grab;

&:hover {
background-color: var(--novel-stone-100);
transition: background-color 0.2s;
}

&:active {
background-color: var(--novel-stone-200);
transition: background-color 0.2s;
cursor: grabbing;
}

&.hide {
opacity: 0;
pointer-events: none;
}

@media screen and (max-width: 600px) {
display: none;
pointer-events: none;
}
}

.dark .drag-handle {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(255, 255, 255, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E");
}
12 changes: 12 additions & 0 deletions examples/with-novel/app/api/uploadthing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { uploadRouter } from "@/uploadthing/server";

import { createRouteHandler } from "uploadthing/next";

export const runtime = "edge";

export const { GET, POST } = createRouteHandler({
router: uploadRouter,
config: {
logLevel: "debug",
},
});
Loading

0 comments on commit 8aa19e4

Please sign in to comment.