diff --git a/documentation/images/Step2.png b/documentation/images/Step2.png new file mode 100644 index 0000000..5efcc6a Binary files /dev/null and b/documentation/images/Step2.png differ diff --git a/documentation/images/Step4Login.png b/documentation/images/Step4Login.png new file mode 100644 index 0000000..18750d2 Binary files /dev/null and b/documentation/images/Step4Login.png differ diff --git a/documentation/images/Step4Logout.png b/documentation/images/Step4Logout.png new file mode 100644 index 0000000..5c91904 Binary files /dev/null and b/documentation/images/Step4Logout.png differ diff --git a/documentation/solid-react-tutorial.md b/documentation/solid-react-tutorial.md new file mode 100644 index 0000000..0c3785c --- /dev/null +++ b/documentation/solid-react-tutorial.md @@ -0,0 +1,240 @@ +# Using LDO to build a Solid Application for React + +Solid separates the application from the storage, allowing users to put their data wherever they choose. Core to achieving this is application interoparability, the ability to use multiple apps on the same dataset. In order to make applications interoperable, Solid is standardized around RDF (Resource Description Framework), a standard for representing data. While RDF is extremely flexible, it is sometimes cumbersome to work with, that's where LDO (Linked Data Objects) comes in. + +In this tutorial, we'll build a web application for Solid using React and LDO. LDO's react library, "@ldobjects/solid-react" is designed to make it easy to manipulate data on a Solid Pod. + +We'll be making a simple micro-blogging website that allows you to write notes and upload photos. + +// TODO insert image + +This tutorial assumes that you are already familiar with React and the overall concepts associated with Solid. + +## 1. Getting Started + +First, we'll initialize the project. LDO is designed to work with TypeScript, so we want to initialize a typescript react project. + +```bash +npx create-react-app my-solid-app --template typescript +cd my-solid-app +``` + +## 2. Setting up a basic app infrastructure + +Before we can use LDO and connect to a Solid Pod, let's get the boilerplace React infrastructure out of the way. We'll set up a single page that renders your blog timeline and lets you make new posts, and we'll do this with 5 components: + +**App.tsx**: Base of the application. +```tsx +import React, { FunctionComponent } from 'react'; +import { Header } from './Header'; +import { Blog } from './Blog'; + +export const App: FunctionComponent = () => { + return ( +
+
+ +
+ ); +} +``` + +**Header.tsx**: A header component that will help the user log in. +```tsx +import { FunctionComponent } from "react"; + +export const Header: FunctionComponent = () => { + return ( +
+

Header

+
+
+ ); +}; +``` + +**Blog.tsx**: The main place for the blog timeline. We'll use this component to list all posts you've made. +```tsx +import { FunctionComponent } from "react"; +import { MakePost } from "./MakePost"; +import { Post } from "./Post"; + +export const Blog: FunctionComponent = () => { + return ( +
+ +
+ +
+ ); +}; +``` + +**MakePost.tsx**: A form to submit new posts. We've already wired it up with form elements to create a text body and upload an image for the post. We just need to fill out the `onSubmit` function. +```tsx +import { FormEvent, FunctionComponent, useCallback, useState } from "react"; + +export const MakePost: FunctionComponent = () => { + const [message, setMessage] = useState(""); + const [selectedFile, setSelectedFile] = useState(); + + const onSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + + // TODO upload functionality + }, + [message, selectedFile] + ); + + return ( +
+ setMessage(e.target.value)} + /> + setSelectedFile(e.target.files?.[0])} + /> + +
+ ); +}; +``` + +**Post.tsx**: A component to render a single post. +```tsx +import { FunctionComponent } from "react"; + +export const Post: FunctionComponent = () => { + return ( +
+

Post

+
+ ); +}; +``` + +When everything's done, run `npm run start` and your application should look like this: + +![What the site should like after step 2 is complete.](./images/Step2.png) + +## Step 3: Installing LDO for Solid and React + +With the main infrastructure set up, let's install LDO's Solid/React library. + +```bash +npm install @ldobjects/solid-react +``` + +This library will give us many useful hooks and components for building a Solid application, but it can't be used unless we wrap the application in a provider. Because we're building a React application in the web browser, we'll wrap the application using the `BrowserSolidLdoProvider`. + +**App.tsx** +```tsx +// ... +import { BrowserSolidLdoProvider } from '@ldobjects/solid-react'; + +export const App: FunctionComponent = () => { + return ( +
+ +
+ + +
+ ); +} +``` + +## 4. Implementing Login/Logout in the header + +Setting up login for a Solid application is easy when you're using ldo's Solid React library. With the `useSolidAuth()` hook, you can get information and methods to setup login. + +In the component below, we use the `session` object for information about the current session including `session.isLoggedIn`, a boolean indicating if the user is currently logged in, and `session.webId` to get the current user's webId. These will automatically update and rerender the component if anything changes about the current session. + +Next we use the `login(issuer: string)` method to initiate a login. Because a Solid Pod could be anywhere onthe web, we first ask the user to enter their Solid issuer then provide that to the login function. + +Finally, the `logout()` function lets you easily trigger a log out. + +**Header.tsx** +```tsx +import { useSolidAuth } from "@ldobjects/solid-react"; +import { FunctionComponent } from "react"; + +export const Header: FunctionComponent = () => { + const { session, login, logout } = useSolidAuth(); + + return ( +
+ {session.isLoggedIn ? ( + // Is the session is logged in +

+ You are logged in with the webId {session.webId}.{" "} + +

+ ) : ( + // If the session is not logged in +

+ You are not Logged In{" "} + +

+ )} +
+
+ ); +}; +``` + +Because `useSolidAuth` is a hook, you can use it anywhere in the application, even components that don't contain buttons for "login" and "logout." For example, we could use the `session` object in `Blog.tsx` to display a message if the user is not logged in. + +```tsx +import { FunctionComponent } from "react"; +import { MakePost } from "./MakePost"; +import { Post } from "./Post"; +import { useSolidAuth } from "@ldobjects/solid-react"; + +export const Blog: FunctionComponent = () => { + const { session } = useSolidAuth(); + if (!session.isLoggedIn) return

No blog available. Log in first.

; + + return ( +
+ // .. +
+ ); +}; + +``` + +Once you've implemented these changes, the application should look like this when logged out: + +![Your site when logged out after completing step 4.](./images/Step4Logout.png) + +And this when logged in: + +![Your site when in out after completing step 4.](./images/Step4Login.png) + +## 5. Setting up a shape +In step 6, we're going to use information from a user's Solid WebId profile. But, before we can do that, we want to set up a shape for the Solid Profile. + +LDO uses ShEx "Shapes" as schemas to describe how data looks in an application. We can get started by using the `init` command line tool to get the project ready to use shapes. + +```bash +npx @ldobjects/cli init +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c1ae5d5..95136a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29412,7 +29412,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", - "dev": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -36373,7 +36372,8 @@ "commander": "^9.3.0", "ejs": "^3.1.8", "fs-extra": "^10.1.0", - "loading-cli": "^1.1.0" + "loading-cli": "^1.1.0", + "prettier": "^3.0.3" }, "bin": { "ldo": "dist/index.js" @@ -48511,6 +48511,7 @@ "fs-extra": "^10.1.0", "jest": "^27.4.2", "loading-cli": "^1.1.0", + "prettier": "*", "rimraf": "^3.0.2", "ts-jest": "^27.0.7" }, @@ -68732,8 +68733,7 @@ "prettier": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", - "dev": true + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==" }, "prettier-linter-helpers": { "version": "1.0.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index e7e0fa9..5114305 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,7 +49,8 @@ "commander": "^9.3.0", "ejs": "^3.1.8", "fs-extra": "^10.1.0", - "loading-cli": "^1.1.0" + "loading-cli": "^1.1.0", + "prettier": "^3.0.3" }, "files": [ "dist" diff --git a/packages/cli/src/init.ts b/packages/cli/src/init.ts index a5567e7..2956d56 100644 --- a/packages/cli/src/init.ts +++ b/packages/cli/src/init.ts @@ -13,8 +13,10 @@ export interface InitOptions { export async function init(initOptions: InitOptions) { // Install dependencies - await exec("npm install ldo --save"); - await exec("npm install ldo-cli @types/shexj @types/jsonld --save-dev"); + await exec("npm install @ldobjects/ldo --save"); + await exec( + "npm install @ldobjects/cli @types/shexj @types/jsonld --save-dev", + ); // Find folder to save to let parentDirectory = initOptions.directory; diff --git a/packages/demo-react/src/Layout.tsx b/packages/demo-react/src/Layout.tsx index 9cdf3a7..bc50128 100644 --- a/packages/demo-react/src/Layout.tsx +++ b/packages/demo-react/src/Layout.tsx @@ -2,19 +2,19 @@ import { useSolidAuth } from "@ldobjects/solid-react"; import React from "react"; import type { FunctionComponent } from "react"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; -import { Dashboard } from "./dashboard/Dashboard"; -import { MediaPage } from "./media/MediaPage"; +import { Blog } from "./blog/Blog"; +import { PostPage } from "./post/PostPage"; import { Header } from "./Header"; import { MainContainerProvider } from "./MainContainerProvider"; const router = createBrowserRouter([ { path: "/", - element: , + element: , }, { path: "/media/:uri", - element: , + element: , }, ]); diff --git a/packages/demo-react/src/dashboard/Dashboard.tsx b/packages/demo-react/src/blog/Blog.tsx similarity index 66% rename from packages/demo-react/src/dashboard/Dashboard.tsx rename to packages/demo-react/src/blog/Blog.tsx index 070ebc4..d9f1009 100644 --- a/packages/demo-react/src/dashboard/Dashboard.tsx +++ b/packages/demo-react/src/blog/Blog.tsx @@ -1,27 +1,27 @@ import React, { Fragment, useContext } from "react"; import type { FunctionComponent } from "react"; import { MainContainerContext } from "../MainContainerProvider"; -import { MediaPost } from "../media/MediaPost"; -import { UploadButton } from "./UploadButton"; +import { Post } from "../post/Post"; +import { MakePost } from "./MakePost"; -export const Dashboard: FunctionComponent = () => { +export const Blog: FunctionComponent = () => { const mainContainer = useContext(MainContainerContext); if (mainContainer === undefined) { return

Loading...

; } if (mainContainer.isDoingInitialFetch()) { - return

Loading Main Container

; + return

Loading Blob

; } return (
- +

{mainContainer.children().map((child) => ( - +
))} diff --git a/packages/demo-react/src/dashboard/UploadButton.tsx b/packages/demo-react/src/blog/MakePost.tsx similarity index 97% rename from packages/demo-react/src/dashboard/UploadButton.tsx rename to packages/demo-react/src/blog/MakePost.tsx index 7e287c2..d9aa58b 100644 --- a/packages/demo-react/src/dashboard/UploadButton.tsx +++ b/packages/demo-react/src/blog/MakePost.tsx @@ -5,7 +5,7 @@ import { v4 } from "uuid"; import { useLdo, useSolidAuth } from "@ldobjects/solid-react"; import { PostShShapeType } from "../.ldo/post.shapeTypes"; -export const UploadButton: FunctionComponent<{ mainContainer: Container }> = ({ +export const MakePost: FunctionComponent<{ mainContainer: Container }> = ({ mainContainer, }) => { const [message, setMessage] = useState(""); diff --git a/packages/demo-react/src/media/MediaPost.tsx b/packages/demo-react/src/post/Post.tsx similarity index 94% rename from packages/demo-react/src/media/MediaPost.tsx rename to packages/demo-react/src/post/Post.tsx index b6f985d..b389385 100644 --- a/packages/demo-react/src/media/MediaPost.tsx +++ b/packages/demo-react/src/post/Post.tsx @@ -5,7 +5,7 @@ import { PostShShapeType } from "../.ldo/post.shapeTypes"; import { useNavigate } from "react-router-dom"; import { PostedBy } from "./PostedBy"; -export const MediaPost: FunctionComponent<{ uri: string }> = ({ uri }) => { +export const Post: FunctionComponent<{ uri: string }> = ({ uri }) => { const navigate = useNavigate(); const mediaResource = useResource(`${uri}index.ttl`); const post = useSubject(PostShShapeType, mediaResource.uri); diff --git a/packages/demo-react/src/media/MediaPage.tsx b/packages/demo-react/src/post/PostPage.tsx similarity index 66% rename from packages/demo-react/src/media/MediaPage.tsx rename to packages/demo-react/src/post/PostPage.tsx index ad446ce..bc5e144 100644 --- a/packages/demo-react/src/media/MediaPage.tsx +++ b/packages/demo-react/src/post/PostPage.tsx @@ -1,16 +1,16 @@ import React from "react"; import type { FunctionComponent } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { MediaPost } from "./MediaPost"; +import { Post } from "./Post"; -export const MediaPage: FunctionComponent = () => { +export const PostPage: FunctionComponent = () => { const navigate = useNavigate(); const { uri } = useParams(); return (
- {uri ? :

No URI Present

} + {uri ? :

No URI Present

}
); }; diff --git a/packages/demo-react/src/media/PostedBy.tsx b/packages/demo-react/src/post/PostedBy.tsx similarity index 100% rename from packages/demo-react/src/media/PostedBy.tsx rename to packages/demo-react/src/post/PostedBy.tsx