24
loading...
This website collects cookies to deliver better user experience
/endpoints
: This endpoint adds new content./endpoints
: Returns all data./endpoints/:id
: Returns data by its id
./endpoints/:id
: Deletes a specific content./endpoints/:id
: Edits a specific content.flex
, pt-4
, text-center
, and rotate-90
that can be composed to build any design directly in your markup.Buttons
, Cards
, BottomSheets
, etc. Instead, Tailwind only has low-level CSS classes. You then use these classes to build your components.strapi-tailwind
: mkdir strapi-tailwind
. Move into the folder: cd strapi-tailwind
.strapi-tailwind
folder will contain our Strapi backend and our Reactjs frontend.npx create-strapi-app strapi-api --quickstart
# OR
yarn create strapi-app strapi-api ---quickstart
strapi-app
folder.localhost:1337
. It will launch the Strapi admin UI panel on localhost:1337/admin
.Bookmark {
title
content
synopsis
}
title
field will be the title of the bookmark. The content
will be the content of the bookmark, and it can be links, notes, etc. Finally, the synopsis
holds a preview of the bookmark's content.bookmark
and click on the Continue
button.Text
field, type in "title".Text
field, type in "content" and select "Long text".Text
field, type in "synopsis"Finish
button. On the page that appears click on the "Save" button on the top-right section of the page./bookmarks
: Create a new bookmark/bookmarks
: Get all bookmarks/bookmarks/:id
: Get a bookmark/bookmarks/:id
: Delete a bookmark/bookmarks/:id
: Update a bookmark.Bookmark
collection. First, click on the Bookmarks
item on the sidebar, click on the + Add New Bookmarks
button on the top-right page."title" -> Become a qualified dev
"content" -> https://raddevon.com/5-projects-to-become-qualified-as-a-web-developer/?ck_subscriber_id=1287376433
"synopsis" -> https://raddevon.com/5-projects-to-become-qua...
"title" -> A Shadaya post
"content" -> When they hit their 30s, the pressure won't be about iphones, clothes, cars, it will be about who got a lovely home, a stable marriage & a happy family. Jealous, bitterness & regrets for the "woke" ones & happiness, joy & fulfilment for the "lame" ones.
"synopsis" -> When they hit their 30s, the pressure won't be about iphones...
"title" -> Twitter post
"content" -> https://twitter.com/Drwhales_/status/1388404654342610944
"synopsis" -> https://twitter.com/Drwhales_/status/138...
Settings
item on the sidebar. Then on Roles
on the right and Public
. BOOKMARK
section, check the Select all
Save
button on the top-right. This will save these changes.Bookmark
collection endpoints are now openly accessible by the Public.npx create-react-app strapi-tailwind
cd strapi-tailwind
.axios
: HTTP library, we will use it to make HTTP requests to the collection's endpoints.react-router-dom
: React library for adding routing system to React apps.
yarn add axios react-router-dom
yarn add --dev tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
yarn add @craco/craco
scripts
in our package.json
to use craco
instead of react-scripts
.craco.config.js
file in the root folder and paste the below code in it:// craco.config.js
module.exports = {
style: {
postcss: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
},
};
npx tailwind init
tailwind.config.js
in our root folder. Open it and paste the below code:module.exports = {
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
src/index.css
file:@tailwind base;
@tailwind components;
@tailwind utilities;
...
/
, this index route will render the bookmarks in the system.Header
: This will contain the header of our app and will display on every page.BookmarkCard
: This component will display a bookmark title and synopsis on the index page.AddBookmarkDialog
: This is a dialog where new bookmarks are added to the system.EditBookmarkDialog
: This is a dialog where an existing bookmark will be edited.ViewBookmarkDialog
: This is a dialog that will display a bookmark's content.BookmarkList
: This component displays all the bookmarks in our system.components
folder. Let's begin to create them:mkdir src/components
cd src/components
mkdir Header
touch Header/index.js
mkdir BookmarkCard
touch BookmarkCard/index.js
mkdir AddBookmarkDialog
touch AddBookmarkDialog/index.js
mkdir EditBookmarkDialog
touch EditBookmarkDialog/index.js
mkdir ViewBookmarkDialog
touch ViewBookmarkDialog/index.js
mkdir BookmarkList
touch BookmarkList/index.js
mkdir CloseIcon
touch CloseIcon/index.js
Header
component:src/components/Header/index.js
:export default function Header() {
return (
<section class="p-4 text-2xl font-bold bg-red-600 text-white mb-4">
<div>Bookmarks</div>
</section>
);
}
className
, everything is done in the class
attribute.p-4
applies padding of 4px to all the corners(top, bottom, left, and right). The text-2xl
gives it a font size of 1.5rem. The font-bold
sets the text to be bold. The bg-red-600
sets the background color of the header to be red in color with a darker opacity. The text-white
sets the text color to be white. The mb-4
sets the bottom margin of the header to be 4px.CopyIcon
component. This component will render an svg icon representation of the copy symbol.src/components/CloseIcon/index.js
:export default function CloseIcon() {
return (
<svg
width="12px"
height="10px"
xmlns="http://www.w3.org/2000/svg"
style={{ cursor: "pointer" }}
fill="white"
>
<path
d="M10.0719417,0.127226812 C10.1612888,0.127226812 10.2403266,0.161591074 10.3090551,0.230319596 L10.3090551,0.230319596 L10.8245191,0.745783513 C10.8932476,0.814512036 10.9276118,0.893549837 10.9276118,0.982896916 C10.9276118,1.07224399 10.8932476,1.1512818 10.8245191,1.22001032 L10.8245191,1.22001032 L6.77297267,5.27155671 L10.8245191,9.3231031 C10.8932476,9.39183162 10.9276118,9.47086942 10.9276118,9.5602165 C10.9276118,9.64956358 10.8932476,9.72860138 10.8245191,9.79732991 L10.8245191,9.79732991 L10.3090551,10.3127938 C10.2403266,10.3815223 10.1612888,10.4158866 10.0719417,10.4158866 C9.98259466,10.4158866 9.90355686,10.3815223 9.83482834,10.3127938 L9.83482834,10.3127938 L5.92809485,6.40509433 C4.98802554,7.34516364 3.68545904,8.64773014 2.02039535,10.3127938 C1.95166683,10.3815223 1.87262903,10.4158866 1.78328195,10.4158866 C1.69393487,10.4158866 1.61489707,10.3815223 1.54616855,10.3127938 L1.03070463,9.79732991 C0.961976106,9.72860138 0.927611845,9.64956358 0.927611845,9.5602165 C0.927611845,9.47086942 0.961976106,9.39183162 1.03070463,9.3231031 L5.08225102,5.27155671 L1.03070463,1.22001032 C0.961976106,1.1512818 0.927611845,1.07224399 0.927611845,0.982896916 C0.927611845,0.893549837 0.961976106,0.814512036 1.03070463,0.745783513 L1.54616855,0.230319596 C1.61489707,0.161591074 1.69393487,0.127226812 1.78328195,0.127226812 C1.87262903,0.127226812 1.95166683,0.161591074 2.02039535,0.230319596 L5.92761184,4.13822681 L9.83482834,0.230319596 C9.88637473,0.178773204 9.94372009,0.146556709 10.0068644,0.133670111 Z"
fillRule="nonzero"
></path>
</svg>
);
}
style={{ cursor: "pointer" }}
to make the cursor transform to a hand icon when the mouse cursor hovers above the copy icon, it gives the users hint that the copy icon is clickable.src/components/BookmarkCard/index.js
:import axios from "axios";
import ViewBookmarkDialog from "./../ViewBookmarkDialog";
import EditBookmarkDialog from "./../EditBookmarkDialog";
import { useState } from "react";
export default function BookmarkCard({ bookmark }) {
const { id, title, content, synopsis } = bookmark;
const [edit, setEdit] = useState(false);
const [view, setView] = useState(false);
const [showCopy, setShowCopy] = useState(false);
var timeout;
function copyBookmark() {
navigator.clipboard.writeText(content).then(
function () {
/* clipboard successfully set */
setShowCopy(true);
clearTimeout(timeout);
timeout = setTimeout(() => {
setShowCopy(false);
}, 1000);
},
function () {
/* clipboard write failed */
setShowCopy(false);
}
);
}
function viewBookmark() {
setView(true);
}
function editBookmark() {
setEdit(true);
}
async function deleteBookmark() {
if (window.confirm("Do you want to delete this bookmark?")) {
await axios.delete("http://localhost:1337/bookmarks/" + id);
window.location.reload();
}
}
return (
<div
style={{ width: "600px" }}
class="border border-gray-200 rounded-md m-3 p-4 shadow-md bg-white hover:shadow-xl"
>
{showCopy ? <Message /> : null}
<div class="py-2">
<h4 class="text-xl font-bold">{title}</h4>
</div>
<div>{synopsis}</div>
<div class="py-2 my-3 flex">
<span
class="cursor-pointer inline mx-1 text-white font-bold py-2 px-4 rounded"
onClick={copyBookmark}
>
<CopyIcon />
</span>
<span
class="cursor-pointer inline mx-1 text-white font-bold py-2 px-4 rounded"
onClick={deleteBookmark}
>
<DeleteIcon />
</span>
<span
class="cursor-pointer inline mx-1 text-white font-bold py-2 px-4 rounded"
onClick={viewBookmark}
>
<ViewIcon />
</span>
<span
class="cursor-pointer inline mx-1 text-white font-bold py-2 px-4 rounded"
onClick={editBookmark}
>
<EditIcon />
</span>
</div>
{view ? (
<ViewBookmarkDialog
bookmark={bookmark}
closeModal={() => setView(false)}
/>
) : null}
{edit ? (
<EditBookmarkDialog
bookmark={bookmark}
closeModal={() => setEdit(false)}
/>
) : null}
</div>
);
}
function DeleteIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="#e73d52"
>
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
</svg>
);
}
function CopyIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="#e73d52"
>
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
</svg>
);
}
function ViewIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="#e73d52"
>
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg>
);
}
function EditIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="#e73d52"
>
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
</svg>
);
}
function Message() {
return (
<div class="z-50 fixed flex p-3 bg-blue-200 rounded-md border-2 border-blue-600 font-bold opacity-90">
<div class="mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="#e73d52"
>
<path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
</div>
<div>
<span class="text-red-600">Copied!</span>
</div>
</div>
);
}
id
, title
, content
, and synopsis
from the bookmark
object.edit
, view
and showCopy
,the edit
and view
states toggles the EditBookmarkDialog
and ViewBookmarkDialog
visibility respectively. The showCopy
toggles a message component when a bookmark's content is copied.timeout
will hold a setTimeout
id, we will use this to clear out timeouts.cursor-pointer
: This makes the mouse take the shape of a hand.rounded-md
: This makes the element's border-radius to be 0.25rem
.inline
: This makes the element to be an inline element.flex
: This sets display:flex;
on the element.hover: shadow-xl
: This sets the box-shadow to be deeper when the element is hovered with a mouse.border
: The border width is 1pxborder-gray-200
: The border color is darker gray.py-2
: This sets the top and bottom padding of the element to be 2px.m-3
: This sets the margin of the element to be 3px.shadow-md
: This sets the box-shadow of the element to be 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
.z-50
: The element has a z-index of 50. This controls the stack order of elements.fixed
: Makes the element a positioned element, in this case, a fixed element.opacity-90
: Makes the element have an opacity of 0.9DeleteIcon
- has svg code that renders a delete icon.ViewIcon
- has svg code that renders an eye icon that denotes viewing an item.CopyIcon
- this renders an svg code that renders a copy icon.EditIcon
- renders svg code that renders an edit icon.span
element with an onClick
attribute. The copyBookmark
function triggered by the copy icon, uses the Clipboard API to copy the contents of the bookmark.viewBookmark
sets the view
state to true which causes the ViewBookmarkDialog
to show up. The bookmark
and a closeModal
function are sent to the component so the component can respectively access the bookmark and close itself using the closeModal
function.editBookmark
function sets the edit
state to true which displays the EditBookmarkDialog
component.deleteBookmark
function deletes the current bookmark from the db. It makes a DELETE HTTP request to localhost:1337/bookmarks/+id
. The id
will be the id of the bookmark, after the request the page is reloaded.src/components/AddBookmarkDialog/index.js
:import axios from "axios";
import { useRef } from "react";
import CloseIcon from "./../CloseIcon";
export default function AddBookmarkDialog({ closeModal }) {
const formRef = useRef();
async function addBookmark() {
var { title, content } = formRef.current;
title = title.value;
content = content.value;
await axios.post("http://localhost:1337/bookmarks", {
title,
content,
synopsis: content.slice(0, 100) + "...",
});
window.location.reload();
}
return (
<div class="modal fixed -top-0 left-0 w-full h-full flex flex-col z-0 items-center">
<div
class="modal-backdrop opacity-70 bg-gray-50 fixed w-full h-full z-10"
onClick={closeModal}
></div>
<div class="modal-content z-20 w-2/5 mt-5 bg-white shadow-md">
<div class="modal-header flex justify-between items-center bg-red-600 p-3 text-white">
<h3 class="text-white font-bold">Add Bookmark</h3>
<span
style={{ padding: "10px", cursor: "pointer" }}
onClick={closeModal}
>
<CloseIcon />
</span>
</div>
<div className="modal-body content m-2 p-5 z-50">
<form ref={formRef}>
<div class="w-full">
<div class="pl-2">
<span>TITLE</span>
</div>
<input
type="text"
class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
placeholder="Type in title.."
name="title"
/>
</div>
<div class="w-full">
<div class="pl-2 mt-3">
<span>CONTENT</span>
</div>
<textarea
type="text"
class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
placeholder="Type in content.."
name="content"
></textarea>
</div>
</form>
</div>
<div className="modal-footer flex justify-between p-4 bg-gray-200">
<button
class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-3 rounded"
onClick={closeModal}
>
Cancel
</button>
<button
class="bg-red-600 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
onClick={addBookmark}
>
Add
</button>
</div>
</div>
</div>
);
}
Add
button calls the addBookmark
function, this function retrieves the values of the bookmark's title and content from the input boxes. localhost:1337/bookmarks
with the synopsis, title, and content retrieved from the UI as payload. The synopsis is generated by slicing off 100 words from the content to get a preview of the content. This request adds the bookmark to our Strapi backend. The page is reloaded which displays the newly added bookmark on the UI.localhost:1337/bookmarks
and displays them.src/components/BookmarkList/index.js
:import BookmarkCard from "./../BookmarkCard";
import axios from "axios";
import { useEffect, useState } from "react";
import AddBookmarkDialog from "./../AddBookmarkDialog";
export default function BookmarkList(params) {
const [bookmarks, setBookmarks] = useState([]);
const [showAddBookmarkDialog, setShowAddBookmarkDialog] = useState(false);
useEffect(async () => {
const data = await axios.get("http://localhost:1337/bookmarks");
setBookmarks(data?.data);
}, []);
return (
<div class="flex flex-col flex-wrap justify-center">
<div class="m-2 p-2">
<button
onClick={() => setShowAddBookmarkDialog(true)}
class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Add Bookmark
</button>
</div>
<div>
{bookmarks
?.sort((a, b) => b.created_at.localeCompare(a.created_at))
.map((bookmark, i) => (
<BookmarkCard bookmark={bookmark} key={i} />
))}
</div>
{showAddBookmarkDialog ? (
<AddBookmarkDialog closeModal={() => setShowAddBookmarkDialog(false)} />
) : null}
</div>
);
}
bookmarks
. This is an array state that will hold the bookmarks fetched from our Strapi backend.useEffect
hook to call the localhost:1337/bookmarks endpoint. The returned bookmarks are set to the bookmarks
state. The bookmarks
are then rendered using the Array#map
method.Add Bookmark
button displays the AddBookmarkDialog
component when clicked.src/components/EditBookmarkDialog/index.js
:import axios from "axios";
import { useRef } from "react";
import CloseIcon from "../CloseIcon";
export default function EditBookmarkDialog({ closeModal, bookmark }) {
const formRef = useRef();
async function editBookmark() {
var { title, content } = formRef.current;
title = title.value;
content = content.value;
await axios.put("http://localhost:1337/bookmarks/" + bookmark?.id, {
title,
content,
synopsis: content.slice(0, 100) + "...",
});
window.location.reload();
}
return (
<div class="modal fixed -top-0 left-0 w-full h-full flex flex-col z-0 items-center">
<div
class="modal-backdrop opacity-70 bg-gray-50 fixed w-full h-full z-10"
onClick={closeModal}
></div>
<div class="modal-content z-20 w-2/5 mt-5 bg-white shadow-md">
<div class="modal-header flex justify-between items-center bg-red-600 p-3 text-white">
<h3 class="text-white font-bold">Edit Bookmark</h3>
<span
style={{ padding: "10px", cursor: "pointer" }}
onClick={closeModal}
>
<CloseIcon />
</span>
</div>
<div className="modal-body content m-2 p-5 z-50">
<form ref={formRef}>
<div class="w-full">
<div class="pl-2">
<span>TITLE</span>
</div>
<input
type="text"
class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
placeholder="Type in title..."
defaultValue={bookmark?.title}
name="title"
/>
</div>
<div class="w-full">
<div class="pl-2 mt-3">
<span>CONTENT</span>
</div>
<textarea
type="text"
class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
placeholder="Type in content..."
defaultValue={bookmark?.content}
name="content"
></textarea>
</div>
</form>
</div>
<div className="modal-footer flex justify-between p-4 bg-gray-200">
<button
class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-3 rounded"
onClick={closeModal}
>
Cancel
</button>
<button
class="bg-red-600 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
onClick={editBookmark}
>
Save
</button>
</div>
</div>
</div>
);
}
bookmark
.Save
button calls the editBookmark
function, this function collects the values of the bookmark's from the input boxes. It generates a synopsis from the content, then makes an HTTP PUT request to localhost:1337/bookmarks/+id
. synopsis
, title
, and content
are sent as payload. The id
will be the id of the bookmark, this enables Strapi to edit the bookmark with the sent payload.src/components/ViewBookmarkDialog/index.js
:import CloseIcon from "./../CloseIcon";
export default function ViewBookmarkDialog({ closeModal, bookmark }) {
return (
<div class="modal fixed -top-0 left-0 w-full h-full flex flex-col z-0 items-center">
<div
class="modal-backdrop opacity-70 bg-gray-50 fixed w-full h-full z-10"
onClick={closeModal}
></div>
<div class="modal-content z-20 w-2/5 mt-5 bg-white shadow-md">
<div class="modal-header flex justify-between items-center bg-red-600 p-3 text-white">
<h3 class="text-white font-bold">View Bookmark</h3>
<span
style={{ padding: "10px", cursor: "pointer" }}
onClick={closeModal}
>
<CloseIcon />
</span>
</div>
<div className="modal-body content m-2 p-5 z-50">
<div class="w-full">
<div class="pl-2">
<span>TITLE</span>
</div>
<input
type="text"
class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
placeholder="Type in title.."
defaultValue={bookmark?.title}
disabled={true}
/>
</div>
<div class="w-full">
<div class="pl-2 mt-3">
<span>CONTENT</span>
</div>
<textarea
type="text"
class="border-gray-200 border-2 w-full m-2 p-2 rounded-md"
placeholder="Type in content.."
disabled={true}
defaultValue={bookmark?.content}
></textarea>
</div>
</div>
<div className="modal-footer flex justify-between p-4 bg-gray-200">
<button
class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-3 rounded"
onClick={closeModal}
>
Close
</button>
</div>
</div>
</div>
);
}
bookmark
object and closeModal
function from its props. It displays the title and the content from the bookmark object. The closeModal
function closes the component.App
component, paste the below code to src/App.js
:import "./App.css";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Header from "./components/Header";
import BookmarkList from "./components/BookmarkList";
function App() {
return (
<>
<Header />
<div class="container bg-gray-100">
<head>
<title>Bookmark</title>
<link rel="icon" href="/favicon.ico" />
</head>
<main class="flex justify-center mx-86">
<BrowserRouter>
<Switch>
<Route exact path="/">
<BookmarkList />
</Route>
<Route path="*">
<BookmarkList />
</Route>{" "}
</Switch>
</BrowserRouter>
</main>
</div>
</>
);
}
export default App;
/
index route. We used the Route
component from react-touter-dom
to render the BookmarkList
component when the index route /
is navigated.Header
component is outside the BrowserRouter
, this makes it render on every page in our routing system.div
element below the Header
is set to have container
in its class
attribute. We want the div
element to center its content. To do that we have to go to the tailwind.config.js
file and make an addition to the theme.extend
object.tailwind.config.js
:module.exports = {
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
container: {
center: true,
},
},
},
variants: {
extend: {},
},
plugins: [],
};
div.container
will center its content.yarn start
yarn develop