Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: support markdown (#215)

* chore: support markdown

* fix: deps

* fix: remove log

authored by

Maximilian Kaske and committed by
GitHub
4637e644 727b97bb

+175 -23
+3
apps/web/package.json
··· 68 68 "react-tweet": "^3.1.1", 69 69 "reading-time": "1.5.0", 70 70 "rehype-pretty-code": "0.10.0", 71 + "rehype-react": "7.2.0", 72 + "remark-parse": "10.0.2", 73 + "remark-rehype": "10.1.0", 71 74 "resend": "0.15.3", 72 75 "shiki": "0.14.3", 73 76 "stripe": "12.17.0",
+16
apps/web/src/components/content/preview.tsx
··· 1 + "use client"; 2 + 3 + import { useProcessor } from "@/hooks/use-preprocessor"; 4 + 5 + interface Props { 6 + md?: string; 7 + } 8 + 9 + export function Preview({ md }: Props) { 10 + const Component = useProcessor(md || ""); 11 + return ( 12 + <div className="prose dark:prose-invert prose-sm prose-headings:font-cal border-input h-[158px] w-full overflow-auto rounded-md border px-3 py-2"> 13 + {Component} 14 + </div> 15 + ); 16 + }
+23 -11
apps/web/src/components/forms/incident-form.tsx
··· 1 - // move into @/components/forms/ later 2 1 "use client"; 3 2 4 3 import * as React from "react"; ··· 14 13 StatusEnum, 15 14 } from "@openstatus/db/src/schema"; 16 15 16 + import { Preview } from "@/components/content/preview"; 17 17 import { Icons } from "@/components/icons"; 18 18 import { LoadingAnimation } from "@/components/loading-animation"; 19 19 import { Button } from "@/components/ui/button"; ··· 30 30 } from "@/components/ui/form"; 31 31 import { Input } from "@/components/ui/input"; 32 32 import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; 33 + import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 33 34 import { Textarea } from "@/components/ui/textarea"; 34 35 import { useToast } from "@/components/ui/use-toast"; 35 36 import { statusDict } from "@/data/incidents-dictionary"; ··· 170 171 <div className="bg-accent/40 border-border col-span-full -m-3 grid gap-6 rounded-lg border p-3 sm:grid-cols-6"> 171 172 <FormField 172 173 control={form.control} 173 - name="message" // TODO: support markdown and add a `Tabs` switch between "Write" and "Preview" 174 + name="message" 174 175 render={({ field }) => ( 175 176 <FormItem className="sm:col-span-4"> 176 177 <FormLabel>Message</FormLabel> 177 - <FormControl> 178 - <Textarea 179 - placeholder="We are encountering..." 180 - className="w-full resize-none" 181 - rows={7} 182 - {...field} 183 - /> 184 - </FormControl> 178 + <Tabs defaultValue="write"> 179 + <TabsList> 180 + <TabsTrigger value="write">Write</TabsTrigger> 181 + <TabsTrigger value="preview">Preview</TabsTrigger> 182 + </TabsList> 183 + <TabsContent value="write"> 184 + <FormControl> 185 + <Textarea 186 + placeholder="We are encountering..." 187 + className="h-auto w-full resize-none" 188 + rows={7} 189 + {...field} 190 + /> 191 + </FormControl> 192 + </TabsContent> 193 + <TabsContent value="preview"> 194 + <Preview md={form.getValues("message")} /> 195 + </TabsContent> 196 + </Tabs> 185 197 <FormDescription> 186 - Tell your user what&apos;s happening. 198 + Tell your user what&apos;s happening. Supports markdown. 187 199 </FormDescription> 188 200 <FormMessage /> 189 201 </FormItem>
+23 -11
apps/web/src/components/forms/incident-update-form.tsx
··· 1 - // move into @/components/forms/ later 2 1 "use client"; 3 2 4 3 import * as React from "react"; ··· 13 12 StatusEnum, 14 13 } from "@openstatus/db/src/schema"; 15 14 15 + import { Preview } from "@/components/content/preview"; 16 16 import { Icons } from "@/components/icons"; 17 17 import { LoadingAnimation } from "@/components/loading-animation"; 18 18 import { Button } from "@/components/ui/button"; ··· 27 27 FormMessage, 28 28 } from "@/components/ui/form"; 29 29 import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; 30 + import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 30 31 import { Textarea } from "@/components/ui/textarea"; 31 32 import { useToast } from "@/components/ui/use-toast"; 32 33 import { statusDict } from "@/data/incidents-dictionary"; ··· 128 129 /> 129 130 <FormField 130 131 control={form.control} 131 - name="message" // TODO: support markdown and add a `Tabs` switch between "Write" and "Preview" 132 + name="message" 132 133 render={({ field }) => ( 133 134 <FormItem className="sm:col-span-4"> 134 135 <FormLabel>Message</FormLabel> 135 - <FormControl> 136 - <Textarea 137 - placeholder="We are encountering..." 138 - className="w-full resize-none" 139 - rows={7} 140 - {...field} 141 - /> 142 - </FormControl> 136 + <Tabs defaultValue="write"> 137 + <TabsList> 138 + <TabsTrigger value="write">Write</TabsTrigger> 139 + <TabsTrigger value="preview">Preview</TabsTrigger> 140 + </TabsList> 141 + <TabsContent value="write"> 142 + <FormControl> 143 + <Textarea 144 + placeholder="We are encountering..." 145 + className="h-auto w-full resize-none" 146 + rows={7} 147 + {...field} 148 + /> 149 + </FormControl> 150 + </TabsContent> 151 + <TabsContent value="preview"> 152 + <Preview md={form.getValues("message")} /> 153 + </TabsContent> 154 + </Tabs> 143 155 <FormDescription> 144 - Tell your user what&apos;s happening. 156 + Tell your user what&apos;s happening. Supports markdown. 145 157 </FormDescription> 146 158 <FormMessage /> 147 159 </FormItem>
+23 -1
apps/web/src/components/incidents/events.tsx
··· 11 11 import { Badge } from "@/components/ui/badge"; 12 12 import { Button } from "@/components/ui/button"; 13 13 import { statusDict } from "@/data/incidents-dictionary"; 14 + import { useProcessor } from "@/hooks/use-preprocessor"; 14 15 import { cn } from "@/lib/utils"; 15 16 import { DeleteIncidentUpdateButtonIcon } from "../../app/app/(dashboard)/[workspaceSlug]/incidents/_components/delete-incident-update"; 16 17 ··· 78 79 {label} 79 80 </Badge> 80 81 </div> 81 - <p className="max-w-3xl text-sm">{update.message}</p> 82 + {/* <p className="max-w-3xl text-sm">{update.message}</p> */} 83 + <EventMessage message={update.message} /> 82 84 </div> 83 85 ); 84 86 })} ··· 93 95 </div> 94 96 ); 95 97 } 98 + 99 + function EventMessage({ 100 + message, 101 + className, 102 + }: { 103 + message: string; 104 + className?: string; 105 + }) { 106 + const Component = useProcessor(message); 107 + return ( 108 + <div 109 + className={cn( 110 + "prose dark:prose-invert prose-sm prose-headings:font-cal", 111 + className, 112 + )} 113 + > 114 + {Component} 115 + </div> 116 + ); 117 + }
+31
apps/web/src/hooks/use-preprocessor.tsx
··· 1 + import type { AnchorHTMLAttributes } from "react"; 2 + import { createElement, Fragment, useEffect, useState } from "react"; 3 + import rehypeReact from "rehype-react"; 4 + import remarkParse from "remark-parse"; 5 + import remarkRehype from "remark-rehype"; 6 + import { unified } from "unified"; 7 + 8 + export function useProcessor(text: string) { 9 + const [Content, setContent] = useState<React.ReactNode>(null); 10 + 11 + useEffect(() => { 12 + unified() 13 + .use(remarkParse) 14 + .use(remarkRehype) 15 + .use(rehypeReact, { 16 + createElement, 17 + Fragment, 18 + components: { 19 + a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => { 20 + return <a target="_blank" rel="noreferrer" {...props} />; 21 + }, 22 + }, 23 + }) 24 + .process(text) 25 + .then((file) => { 26 + setContent(file.result); 27 + }); 28 + }, [text]); 29 + 30 + return Content; 31 + }
+56
pnpm-lock.yaml
··· 249 249 rehype-pretty-code: 250 250 specifier: 0.10.0 251 251 version: 0.10.0(shiki@0.14.3) 252 + rehype-react: 253 + specifier: 7.2.0 254 + version: 7.2.0(@types/react@18.2.12) 255 + remark-parse: 256 + specifier: 10.0.2 257 + version: 10.0.2 258 + remark-rehype: 259 + specifier: 10.1.0 260 + version: 10.1.0 252 261 resend: 253 262 specifier: 0.15.3 254 263 version: 0.15.3 ··· 2331 2340 globby: 11.1.0 2332 2341 jju: 1.4.0 2333 2342 read-yaml-file: 1.1.0 2343 + dev: false 2344 + 2345 + /@mapbox/hast-util-table-cell-style@0.2.0: 2346 + resolution: {integrity: sha512-gqaTIGC8My3LVSnU38IwjHVKJC94HSonjvFHDk8/aSrApL8v4uWgm8zJkK7MJIIbHuNOr/+Mv2KkQKcxs6LEZA==} 2347 + engines: {node: '>=12'} 2348 + dependencies: 2349 + unist-util-visit: 1.4.1 2334 2350 dev: false 2335 2351 2336 2352 /@mdx-js/esbuild@2.3.0(esbuild@0.18.17): ··· 8056 8072 resolution: {integrity: sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==} 8057 8073 dev: false 8058 8074 8075 + /hast-to-hyperscript@10.0.3: 8076 + resolution: {integrity: sha512-NuBoUStp4fRwmvlfbidlEiRSTk0gSHm+97q4Xn9CJ10HO+Py7nlTuDi6RhM1qLOureukGrCXLG7AAxaGqqyslQ==} 8077 + dependencies: 8078 + '@types/unist': 2.0.7 8079 + comma-separated-tokens: 2.0.3 8080 + property-information: 6.2.0 8081 + space-separated-tokens: 2.0.2 8082 + style-to-object: 0.4.2 8083 + web-namespaces: 2.0.1 8084 + dev: false 8085 + 8059 8086 /hast-util-from-dom@4.2.0: 8060 8087 resolution: {integrity: sha512-t1RJW/OpJbCAJQeKi3Qrj1cAOLA0+av/iPFori112+0X7R3wng+jxLA+kXec8K4szqPRGI8vPxbbpEYvvpwaeQ==} 8061 8088 dependencies: ··· 11241 11268 shiki: 0.14.3 11242 11269 dev: false 11243 11270 11271 + /rehype-react@7.2.0(@types/react@18.2.12): 11272 + resolution: {integrity: sha512-MHYyCHka+3TtzBMKtcuvVOBAbI1HrfoYA+XH9m7/rlrQQATCPwtJnPdkxKKcIGF8vc9mxqQja9r9f+FHItQeWg==} 11273 + peerDependencies: 11274 + '@types/react': '>=17' 11275 + dependencies: 11276 + '@mapbox/hast-util-table-cell-style': 0.2.0 11277 + '@types/hast': 2.3.5 11278 + '@types/react': 18.2.12 11279 + hast-to-hyperscript: 10.0.3 11280 + hast-util-whitespace: 2.0.1 11281 + unified: 10.1.2 11282 + dev: false 11283 + 11244 11284 /rehype-slug@5.1.0: 11245 11285 resolution: {integrity: sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw==} 11246 11286 dependencies: ··· 12572 12612 resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} 12573 12613 dev: false 12574 12614 12615 + /unist-util-is@3.0.0: 12616 + resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==} 12617 + dev: false 12618 + 12575 12619 /unist-util-is@5.2.1: 12576 12620 resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} 12577 12621 dependencies: ··· 12615 12659 dependencies: 12616 12660 '@types/unist': 2.0.7 12617 12661 12662 + /unist-util-visit-parents@2.1.2: 12663 + resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==} 12664 + dependencies: 12665 + unist-util-is: 3.0.0 12666 + dev: false 12667 + 12618 12668 /unist-util-visit-parents@4.1.1: 12619 12669 resolution: {integrity: sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==} 12620 12670 dependencies: ··· 12633 12683 dependencies: 12634 12684 '@types/unist': 3.0.0 12635 12685 unist-util-is: 6.0.0 12686 + dev: false 12687 + 12688 + /unist-util-visit@1.4.1: 12689 + resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==} 12690 + dependencies: 12691 + unist-util-visit-parents: 2.1.2 12636 12692 dev: false 12637 12693 12638 12694 /unist-util-visit@3.1.0: