From 9aff2efbf0b21f0867f5d30afe8c7948d7ab5ab4 Mon Sep 17 00:00:00 2001 From: Spencer Chang Date: Sat, 17 Aug 2024 21:40:16 -0700 Subject: [PATCH] WORKSHOP --- packages/react/examples/LiveChat.scss | 129 +++++++++++++++ packages/react/examples/LiveChat.tsx | 121 ++++++++++++++ packages/react/src/index.tsx | 8 +- website/events/gray-area/gray-area.scss | 66 +++++++- website/events/gray-area/gray-area.tsx | 206 ++++++++++++++++++++++-- 5 files changed, 510 insertions(+), 20 deletions(-) create mode 100644 packages/react/examples/LiveChat.scss create mode 100644 packages/react/examples/LiveChat.tsx diff --git a/packages/react/examples/LiveChat.scss b/packages/react/examples/LiveChat.scss new file mode 100644 index 0000000..9f62ce6 --- /dev/null +++ b/packages/react/examples/LiveChat.scss @@ -0,0 +1,129 @@ +.live-chat { + width: 300px; + height: 400px; + border: 2px solid #000; + background-color: #fff; + display: flex; + flex-direction: column; + font-family: "Comic Sans MS", cursive, sans-serif; + margin-right: 10px; + transition: height 0.3s ease; + + &.minimized { + height: 40px; + } + + .chat-header { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #007700; + color: #fff; + padding: 5px; + cursor: pointer; + + h2 { + margin: 0; + font-size: 1em; + } + + button { + background: none; + border: none; + color: #fff; + cursor: pointer; + } + } + + .chat-window { + flex: 1; + padding: 10px; + overflow-y: auto; + border-bottom: 2px solid #000; + background-color: #f0f0f0; + + .chat-message { + margin-bottom: 10px; + padding: 5px; + border-radius: 5px; + background-color: #e0e0e0; + + strong { + color: #007700; + } + } + } + + .chat-input { + display: flex; + padding: 10px; + background-color: #d0d0d0; + + input { + flex: 1; + padding: 5px; + border: 1px solid #000; + border-radius: 3px; + margin-right: 5px; + } + + button { + padding: 5px 10px; + border: 1px solid #000; + border-radius: 3px; + background-color: #007700; + color: #fff; + cursor: pointer; + + &:hover { + background-color: #005500; + } + } + } +} + +.live-chat-controller { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + display: flex; + flex-direction: column; + + .chat-creation { + display: flex; + background-color: #007700; + color: #fff; + padding: 10px; + display: flex; + + input { + flex: 1; + padding: 5px; + border: 1px solid #000; + border-radius: 3px; + margin-right: 5px; + } + + button { + padding: 5px 10px; + border: 1px solid #000; + border-radius: 3px; + background-color: #fff; + color: #007700; + cursor: pointer; + + &:hover { + background-color: #005500; + color: #fff; + } + } + } + + .chat-list { + display: flex; + flex-wrap: nowrap; + align-items: flex-end; + overflow-x: auto; + } +} diff --git a/packages/react/examples/LiveChat.tsx b/packages/react/examples/LiveChat.tsx new file mode 100644 index 0000000..d2f7236 --- /dev/null +++ b/packages/react/examples/LiveChat.tsx @@ -0,0 +1,121 @@ +import React, { useState } from "react"; +import { withSharedState } from "@playhtml/react"; +import { PlayProvider } from "@playhtml/react"; +import "./LiveChat.scss"; + +interface ChatMessage { + id: string; + name: string; + text: string; + color?: string; +} + +interface LiveChatProps { + name: string; +} + +const LiveChat = withSharedState( + { + defaultData: { messages: [] as ChatMessage[] }, + }, + ({ data, setData }, { name }) => { + const [newMessage, setNewMessage] = useState(""); + const [isMinimized, setIsMinimized] = useState(true); + const userId = localStorage.getItem("userId") || "unknown"; + const userName = Boolean(localStorage.getItem("username")) + ? JSON.parse(localStorage.getItem("username")) || "Anonymous" + : "Anonymous"; + + const handleSend = () => { + if (newMessage.trim()) { + const message = { + id: userId, + name: userName, + text: newMessage, + // this comes from cursor party, if not defined it just defaults to black + // @ts-ignore + color: window.cursors.color, + }; + setData({ messages: [...data.messages, message] }); + setNewMessage(""); + } + }; + + // TODO: add read in local store, handle notifications + return ( +
+
setIsMinimized(!isMinimized)} + > +

{name}

+ +
+ {!isMinimized && ( + <> +
+ {data.messages.map((msg, index) => ( +
+ + {msg.name} + + : {msg.text} +
+ ))} +
+
+ setNewMessage(e.target.value)} + placeholder="Type your message..." + /> + +
+ + )} +
+ ); + } +); + +export const LiveChatController = withSharedState( + { + defaultData: { chatNames: [] as string[] }, + }, + ({ data, setData }) => { + const [newChatName, setNewChatName] = useState(""); + + const handleCreateChat = () => { + if (newChatName.trim() && !data.chatNames.includes(newChatName)) { + setData({ chatNames: [...data.chatNames, newChatName] }); + setNewChatName(""); + } + }; + + return ( +
+
+ {data.chatNames.map((name, index) => ( + + ))} +
+
+
+ setNewChatName(e.target.value)} + placeholder="Enter chat name..." + /> + +
+
+
+ ); + } +); diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 414cde5..e598826 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -294,7 +294,9 @@ export function withSharedState( // }; // } -// @deprecated use withSharedState instead +/** + * @deprecated use withSharedState instead + */ export const withPlay =

() => ( @@ -304,7 +306,9 @@ export const withPlay = ) => React.ReactElement ) => withPlayBase(playConfig, component); -// @deprecated use withSharedState instead +/** + * @deprecated use withSharedState instead + */ export function withPlayBase( playConfig: WithPlayProps | ((props: P) => WithPlayProps), component: ( diff --git a/website/events/gray-area/gray-area.scss b/website/events/gray-area/gray-area.scss index 45a8ce4..5498e81 100644 --- a/website/events/gray-area/gray-area.scss +++ b/website/events/gray-area/gray-area.scss @@ -125,10 +125,13 @@ h3 { } section { - margin: 8em 0; + margin: 15em 0; &:nth-child(2) { margin-top: 0; } + &:last-child { + margin-bottom: 0; + } } .retro-timer { @@ -169,3 +172,64 @@ section { background: yellow; color: #808080; } + +#guestbook { + background: #d1e7ff; + border: 2px solid #0078d7; + border-radius: 10px; + padding: 20px; + width: 100%; + max-width: 600px; + margin: 20px auto; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + font-family: "Comic Sans MS", cursive, sans-serif; +} + +.guestbook-actions { + display: flex; + flex-direction: column; + margin-bottom: 1em; + gap: 0.3em; +} + +.guestbook-actions span { +} + +.guestbook-actions textarea { + height: 100px; + margin-bottom: 10px; + padding: 10px; + border: 2px solid #0078d7; + border-radius: 5px; + font-family: "Comic Sans MS", cursive, sans-serif; +} + +.guestbook-actions button { + background: #0078d7; + color: white; + border: none; + padding: 10px 20px; + font-size: 1em; + align-self: flex-end; + cursor: pointer; + border-radius: 5px; + transition: background 0.3s; +} + +.guestbook-actions button:hover { + background: #005bb5; +} + +.guestbook-entry { + background: white; + border: 1px solid #0078d7; + gap: 0.1em; + border-radius: 5px; + padding: 10px; + margin-bottom: 10px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); +} + +.guestbook-entry b { + color: #0078d7; +} diff --git a/website/events/gray-area/gray-area.tsx b/website/events/gray-area/gray-area.tsx index 244bd84..1109ec7 100644 --- a/website/events/gray-area/gray-area.tsx +++ b/website/events/gray-area/gray-area.tsx @@ -2,9 +2,7 @@ import ReactDOM from "react-dom"; import "./gray-area.scss"; import randomColor from "randomcolor"; import React, { useEffect } from "react"; -import { PlayProvider } from "@playhtml/react"; -import { withPlay } from "@playhtml/react"; -import { TagType } from "@playhtml/common"; +import { PlayProvider, withSharedState } from "@playhtml/react"; const NumCursors = 50; @@ -51,7 +49,7 @@ function Cursors() { ); } -const Timer = withPlay()( +const Timer = withSharedState( { defaultData: { time: 0, isRunning: false }, }, @@ -117,7 +115,32 @@ function PlayhtmlToolBox() { ); } +function useStickyState( + key: string, + defaultValue: T, + onUpdateCallback?: (value: T) => void +): [T, (value: T) => void] { + const [value, setValue] = React.useState(() => { + const stickyValue = window.localStorage.getItem(key); + return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue; + }); + React.useEffect(() => { + window.localStorage.setItem(key, JSON.stringify(value)); + onUpdateCallback?.(value); + }, [key, value]); + return [value, setValue]; +} + function Main() { + const [name, setName] = useStickyState( + "username", + null, + (newName) => { + window.cursors?.setName(newName); + } + ); + const [from, setFrom] = useStickyState("from", null); + return ( @@ -125,7 +148,31 @@ function Main() {

hi welcome to "neighborhood internets"

- what will we do today +

+ get settled, introduce yourself to your neighbor, and open your + laptop to do the following: +

    +
  1. + open{" "} + + https://playhtml.fun/events/gray-area + +
  2. +
  3. + join this{" "} + discord channel (we'll + be using it as our class chat to share links, etc.) +
  4. +
+

+

+ we'll wait a bit for everyone to get here to get started :) in the + meanwhile, if you're on this site, you can play around with some of + the objects here. +

+
+
+

what will we do today

  • make a collaborative website experience using playhtml
  • - play each other's experiences and open up our ideas of what the - web can be + play each other's websites and expand our idea of what the web can + be
  • have fun :)
@@ -215,17 +262,30 @@ function Main() {

+
+

+ + (and thank you to Andre from Gray Area for helping us set up & + Gray Area for hosting us!) + +

let's get to know each other a bit

  • - what's your name? + what's your name?{" "} + { + setName(e.target.value); + }} + />
  • where are you from?
  • -
  • what are you excited for today and/or what do you wish the web was like? @@ -283,14 +343,21 @@ function Main() {

    let's dip our toes with making something!!!!!

      -
    • copy this glitch template
    • -
    • you have one of each example on here
    • -
    • I'll quickly explain what's happening here
    • +
    • + copy this{" "} + + glitch template + +
    • let's play around for 10 mins here. I'd suggest finding different ways to replace the image, etc.
    • -
    • let's focus on a single interaction
    • +
    • + let's focus on trying out all the different capabilities with + different assets. try playing around with those around you to get + a feel for how it feels to be together on the same site! +
    {/* TODO:

    demonstrations for each capability

    */}

    @@ -352,14 +419,119 @@ function Main() {

    let's memorialize what we did here today!

    {/* TODO: hook up interactive sign up */} -

    sign the guestbook

    + + sign the guestbook and if you'd like, leave a link to your website! + +
    +
    + +
    - ); } +interface GuestbookEntry { + name: string; + from: string; + color?: string; + message: string; + timestamp: number; +} + +const Guestbook = withSharedState( + { defaultData: [] as GuestbookEntry[] }, + ({ data, setData }, { name, from }: { name: string; from: string }) => { + const [message, setMessage] = React.useState(""); + + const handleSubmit = () => { + if (message.trim()) { + setData([ + ...data, + { + name, + from, + color: window?.cursors?.color || undefined, + message, + timestamp: Date.now(), + }, + ]); + setMessage(""); + } + }; + + return ( +
    +
    + + + {name} + + {from ? ` (${from})` : ""} says... + +