diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b24d163..b1e0b29 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,4 +1,4 @@ -name: Code-Coverage +name: Code Coverage on: [push, pull_request] @@ -26,18 +26,18 @@ jobs: # Maps tcp port 5432 on service container to the host - 5432:5432 - strategy: - matrix: - node-version: [14.x] + # strategy: + # matrix: + # node-version: [14.x] steps: - name: Checkout repository uses: actions/checkout@v2 - - name: Set up Node.js ${{ matrix.node-version }} + - name: Set up Node.js 14.x uses: actions/setup-node@v1 with: - node-version: ${{ matrix.node-version }} + node-version: 14.x - name: Cache node modules uses: actions/cache@v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c936d0a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,29 @@ +name: Publish +# Only trigger, when the build workflow succeeded + +on: + workflow_run: + workflows: ["Code Coverage"] + branches: [master] + types: + - completed + # - requested +jobs: + Publish: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Build docker container + run: docker build . -t ghcr.io/$(echo $GITHUB_ACTOR | tr '[:upper:]' '[:lower:]')/linkin:latest -t ghcr.io/$(echo $GITHUB_ACTOR | tr '[:upper:]' '[:lower:]')/linkin:$(echo $GITHUB_SHA | head -c7) + + - name: Login to docker + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{github.actor}} --password-stdin + + - name: Push + run: docker push ghcr.io/$(echo $GITHUB_ACTOR | tr '[:upper:]' '[:lower:]')/linkin:latest +# docker build . -t ghcr.io/aa/linkin:latest -t ghcr.io/aa/linkin:${echo $GITHUB_SHA | head -c7} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bbb0c5..e51ffd2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,13 +73,12 @@ Linkin has 2 main branches 1. [master](https://github.com/RizkyRajitha/linkin/tree/master) branch - will have the code from the latest release. only updates on a release. 2. [dev](https://github.com/RizkyRajitha/linkin/tree/dev) branch - all the development carries out in this branch. the latest code will be available in this branch, **all the pull requests should be made to dev branch** since prs could be tested and modified for the final release phase. -other than the above branches there can feature specific branches for the continence. + other than the above branches there can feature specific branches for the continence. ## Making pull request when making a pull request please create your feature branch using the **[dev](https://github.com/RizkyRajitha/linkin/tree/dev)** branch (`checkout using dev branch`), and develop in it locally. avoid installing additional dependencies unless clarified through a maintainer. make the pr to the **[dev](https://github.com/RizkyRajitha/linkin/tree/dev)** branch. - ## File Structure ``` @@ -239,7 +238,8 @@ there are 6 forms inside the `Formwrapper`.form has been divided into this manne 3. Colors (form that holds color details) [colorform.js](./components/colorform.js) 4. Fonts (form that holds font details) [fontform.js](./components/fontform.js) 5. Link Data (a form that configures links and their respective details, this form have it's how API posing to facilitate live refresh in links) [linksform.js](./components/linksform.js) -6. Updated Password (a form that updates the password) [passwordchangeform.js](./components/passwordchangeform.js) +6. Social Data (a form that configures **social** links and their respective details, this form have it's how API posing to facilitate live refresh in links) [socialform.js](./components/socialform.js) +7. Updated Password (a form that updates the password) [passwordchangeform.js](./components/passwordchangeform.js) #### Preview @@ -257,16 +257,24 @@ Linkin levarages next js built-in [api-routes](https://nextjs.org/docs/api-route this removes the requirement of additional API deployment, making Linkin cleaner and easier to deploy. all the API routes live in [/pages/api/](./pages/api/) directory. -Linkin has 8 API routes +Linkin has 12 API routes: 1. login [login.js](./pages/api/login.js) 2. logout [logout.js](./pages/api/logout.js) 3. changepassword [changepassword.js](./pages/api/changepassword.js) 4. updatepagedata [updatepagedata.js](./pages/api/updatepagedata.js) -5. insertpagelinks [insertpagelinks.js](./pages/api/insertpagelinks.js) -6. updatepagelinks [updatepagelinks.js](./pages/api/updatepagelinks.js) -7. deletepagelink [deletepagelink.js](./pages/api/deletepagelink.js) -8. reorderlinks [reorderlinks.js](./pages/api/reorderlinks.js) +5. insertlinks + - For pages form: [insertlinks.js](./pages/api/pages/insertlinks.js) + - For social form: [insertlinks.js](./pages/api/social/insertlinks.js) +6. updatelinks + - For pages form: [updatelinks.js](./pages/api/pages/updatelinks.js) + - For social form: [updatelinks.js](./pages/api/social/updatelinks.js) +7. deletelink + - For pages form: [deletelink.js](./pages/api/pages/deletelink.js) + - For social form: [deletelink.js](./pages/api/social/deletelink.js) +8. reorderlinks + - For pages form [reorderlinks.js](./pages/api/pages/reorderlinks.js) + - For social form [reorderlinks.js](./pages/api/social/reorderlinks.js) | API | method | data | response | | --------------- | ------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/README.md b/README.md index c39b718..741160e 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ ## Linkin is a customizable self-hosted link tree application. -### 100% Free and Open Source πŸ’― +### Free and Open Source πŸ’― ### Self Hosted, you own your data πŸ’½ -### Customize your link tree with few clicks with a feature-rich dashboard πŸ€– +### Customize your link tree with few clicks using a feature-rich dashboard πŸ€– ### SEO friendly design built using Next js πŸ•ΈοΈ -### Supports 3 one-click deploy hosting providers πŸš€ +### Supports one-click deploy using multiple cloud providers πŸš€
diff --git a/__tests__/prismalinkdata.test.js b/__tests__/prismalinkdata.test.js index 3f56d8e..842f7e6 100644 --- a/__tests__/prismalinkdata.test.js +++ b/__tests__/prismalinkdata.test.js @@ -1,5 +1,5 @@ const path = require("path"); -const Prisma = require("../db/dbconprisma"); +const { default: Prisma } = require("../db/dbconprisma"); require("dotenv").config({ path: path.join(__dirname, "../", ".env"), @@ -10,21 +10,37 @@ const { insertPageLinks, updateLink, deleteLink, + reorderLinks, } = require("../lib/dbfuncprisma"); describe("Test Link data", () => { + beforeAll(async () => { + await Prisma.linkdata.deleteMany(); + await Prisma.linkdata.create({ + data: { + id: 1, + pagedataid: 1, + iconClass: "fas fa-link", + displayText: "Welcome to LinkIn", + linkUrl: "https://github.com/RizkyRajitha/linkin", + bgColor: "#2C6BED", + active: true, + }, + }); + }); + beforeEach(() => { jest.spyOn(console, "log").mockImplementation(() => {}); }); + afterAll(async () => { - await Prisma.default.$disconnect(); + await Prisma.$disconnect(); }); test("get link data", async () => { let { linkData } = await getLinkData(); - //console.info(linkData); + const expectedLink = { - id: 1, pagedataid: 1, iconClass: "fas fa-link", displayText: "Welcome to LinkIn", @@ -40,9 +56,8 @@ describe("Test Link data", () => { test("get link data without active", async () => { let { linkData } = await getLinkData(false); - //console.log(linkData); + const expectedLink = { - id: 1, pagedataid: 1, iconClass: "fas fa-link", displayText: "Welcome to LinkIn", @@ -59,7 +74,6 @@ describe("Test Link data", () => { test("get link data without active", async () => { let { linkData } = await getLinkData(false); const expectedLink = { - id: 1, pagedataid: 1, iconClass: "fas fa-link", displayText: "Welcome to LinkIn", @@ -87,8 +101,6 @@ describe("Test Link data", () => { await insertPageLinks(linkTobeinserted); let updatedLinkData = await getLinkData(); - //console.info(updatedLinkData); - expect(updatedLinkData.linkData).toMatchObject([ ...beforeUpdateLinkData.linkData, linkTobeinserted, @@ -96,10 +108,10 @@ describe("Test Link data", () => { }); test("delete link data ", async () => { - let beforeUpdateLinkData = await getLinkData(); + let beforeUpdateLinkData = await (await getLinkData()).linkData; //console.info(beforeUpdateLinkData.linkData[1].id); - let delelement = beforeUpdateLinkData.linkData.filter((ele) => { + let delelement = beforeUpdateLinkData.filter((ele) => { return ele.id !== 1; }); @@ -108,9 +120,11 @@ describe("Test Link data", () => { //console.info(updatedLinkData); - expect(updatedLinkData.linkData).toMatchObject([ - beforeUpdateLinkData.linkData[0], - ]); + expectedLink = beforeUpdateLinkData.filter((ele) => { + return ele.id === 1; + }); + + expect(updatedLinkData.linkData[0]).toMatchObject(expectedLink[0]); }); test("update link data ", async () => { @@ -136,25 +150,42 @@ describe("Test Link data", () => { // }); }); - test("revert updated link data ", async () => { - const linkTobeUpdated = { - id: 1, + test("reorder links", async () => { + await insertPageLinks({ pagedataid: 1, iconClass: "fas fa-link", - displayText: "Welcome to LinkIn", + displayText: "link2", linkUrl: "https://github.com/RizkyRajitha/linkin", bgColor: "#2C6BED", active: true, - }; + }); - await updateLink(linkTobeUpdated); - let updatedLinkData = await getLinkData(); + let orderList = await ( + await getLinkData() + ).linkData.map((item) => { + return { id: item.id, orderIndex: item.orderIndex }; + }); - //console.info(updatedLinkData); + // console.info(orderList); - expect(updatedLinkData.linkData[0]).toMatchObject(linkTobeUpdated); - // linkData.forEach((element) => { - // expect(element).toMatchObject(expectedLink); - // }); + let reOrderList = orderList.map((item, index) => { + return { id: item.id, orderIndex: index }; + }); + + await reorderLinks(reOrderList); + + let updatedOrderList = await ( + await getLinkData() + ).linkData.map((item) => { + return { orderIndex: item.orderIndex }; + }); + + // expect(updatedLinkData.linkData[0]).toMatchObject(linkTobeUpdated); + + let expectedOrderList = [{ orderIndex: 0 }, { orderIndex: 1 }]; + + expectedOrderList.forEach((element, index) => { + expect(element).toMatchObject(updatedOrderList[index]); + }); }); }); diff --git a/__tests__/prismapagedata.test.js b/__tests__/prismapagedata.test.js index d532e1e..fceb231 100644 --- a/__tests__/prismapagedata.test.js +++ b/__tests__/prismapagedata.test.js @@ -1,5 +1,5 @@ const path = require("path"); -const Prisma = require("../db/dbconprisma"); +const { default: Prisma } = require("../db/dbconprisma"); require("dotenv").config({ path: path.join(__dirname, "../", ".env"), @@ -12,14 +12,54 @@ const { } = require("../lib/dbfuncprisma"); describe("page data functions", () => { + beforeAll(async () => { + await Prisma.linkdata.deleteMany(); + await Prisma.pagedata.deleteMany(); + await Prisma.pagedata.create({ + data: { + id: 1, + avatarUrl: + "https://res.cloudinary.com/dijjqfsto/image/upload/v1621666671/linkin_logo_1_jcuvr3.png", + avatarwidth: "50", + bgColor: "#7ea2ff", + accentColor: "#bdd7ff", + handlerText: "LinkIn", + footerText: "Powered by Linkin", + handlerFontSize: "20", + handlerFontColor: "#ffffff", + }, + }); + + await Prisma.linkdata.createMany({ + data: [ + { + pagedataid: 1, + iconClass: "fas fa-link", + displayText: "Welcome to LinkIn", + linkUrl: "https://github.com/RizkyRajitha/linkin", + bgColor: "#2C6BED", + active: true, + }, + { + pagedataid: 1, + iconClass: "fas fa-link", + displayText: "Inactive", + linkUrl: "https://github.com/RizkyRajitha/linkin", + bgColor: "#2C6BED", + active: false, + }, + ], + }); + }); + beforeEach(() => { jest.spyOn(console, "log").mockImplementation(() => {}); }); afterAll(async () => { - await Prisma.default.$disconnect(); + await Prisma.$disconnect(); }); - test("get page data ", async () => { + test("get pagedata ", async () => { let { pageData } = await getPageData(); const expectedUser = { id: 1, @@ -38,9 +78,8 @@ describe("page data functions", () => { expect(pageData).toMatchObject(expectedUser); }); - test("get page data with links data with active links", async () => { + test("get page data with all links data", async () => { let pageDatawLinks = await getPageDatawLinkData(); - console.log(pageDatawLinks); const expectedPageDataWLinks = { pageData: { id: 1, @@ -62,22 +101,34 @@ describe("page data functions", () => { }, linkData: [ { - id: 1, pagedataid: 1, iconClass: "fas fa-link", displayText: "Welcome to LinkIn", linkUrl: "https://github.com/RizkyRajitha/linkin", bgColor: "#2C6BED", active: true, + accentColor: null, + borderRadius: null, + textColor: null, + }, + { + pagedataid: 1, + iconClass: "fas fa-link", + displayText: "Inactive", + linkUrl: "https://github.com/RizkyRajitha/linkin", + bgColor: "#2C6BED", + active: false, + accentColor: null, + borderRadius: null, + textColor: null, }, ], }; expect(pageDatawLinks).toMatchObject(expectedPageDataWLinks); }); - test("get page data with links data without active links", async () => { + test("get page data with only active links", async () => { let pageDatawLinks = await getPageDatawLinkData(false); - //console.info(pageDatawLinks); const expectedPageDataWLinks = { pageData: { id: 1, @@ -99,13 +150,15 @@ describe("page data functions", () => { }, linkData: [ { - id: 1, pagedataid: 1, iconClass: "fas fa-link", displayText: "Welcome to LinkIn", linkUrl: "https://github.com/RizkyRajitha/linkin", bgColor: "#2C6BED", active: true, + accentColor: null, + borderRadius: null, + textColor: null, }, ], }; @@ -131,38 +184,8 @@ describe("page data functions", () => { fontFamily: "Roboto", }; - //let updatedPageData = await updatePageData(beforUpdatePageData); - await updatePageData(beforUpdatePageData); - let updatedPageData = await getPageData(); - //console.info(updatedPageData); - - expect(updatedPageData.pageData).toMatchObject(beforUpdatePageData); - }); - - test("revert Updated page data", async () => { - let beforUpdatePageData = { - id: 1, - avatarUrl: - "https://res.cloudinary.com/dijjqfsto/image/upload/v1621666671/linkin_logo_1_jcuvr3.png", - avatarheight: null, - avatarwidth: "50", - bgColor: "#7ea2ff", - accentColor: "#bdd7ff", - handlerText: "LinkIn", - handlerLink: null, - footerText: 'Powered by Linkin', - bgImgUrl: null, - handlerFontSize: "20", - handlerFontColor: "#ffffff", - active: true, - fontFamily: null, - fontUrl: null, - }; - - //let updatedPageData = await updatePageData(beforUpdatePageData); await updatePageData(beforUpdatePageData); let updatedPageData = await getPageData(); - //console.info(updatedPageData); expect(updatedPageData.pageData).toMatchObject(beforUpdatePageData); }); diff --git a/__tests__/prismausers.test.js b/__tests__/prismausers.test.js index 379789b..e23854e 100644 --- a/__tests__/prismausers.test.js +++ b/__tests__/prismausers.test.js @@ -1,23 +1,33 @@ const path = require("path"); + require("dotenv").config({ path: path.join(__dirname, "../", ".env"), }); -const Prisma = require("../db/dbconprisma"); +const { default: Prisma } = require("../db/dbconprisma"); const { getUser, changePassword } = require("../lib/dbfuncprisma"); describe("Test User functions", () => { + beforeAll(async () => { + await Prisma.users.deleteMany(); + await Prisma.users.create({ + data: { + username: "admin", + password: + "$2b$10$gKoU.xdV9vrGY2wEW0KAnuBmQeYxOUgXRHS9f8Sgx40m7kxpejddG", + }, + }); + }); + beforeEach(() => { jest.spyOn(console, "log").mockImplementation(() => {}); }); afterAll(async () => { - await Prisma.default.$disconnect(); + await Prisma.$disconnect(); }); test("get admin user", async () => { - //console.warn("test 1 --------------------"); - let user = await getUser("admin"); const expectedUser = { @@ -28,8 +38,6 @@ describe("Test User functions", () => { }); test("change Password of admin", async () => { - //console.warn("test 2 --------------------"); - try { let newUserPasswordData = { username: "admin", @@ -49,29 +57,4 @@ describe("Test User functions", () => { console.error(error); } }); - - test("revert Password of admin", async () => { - //console.warn("test 2 --------------------"); - - try { - let newUserPasswordData = { - username: "admin", - newhashedpassword: - "$2b$10$gKoU.xdV9vrGY2wEW0KAnuBmQeYxOUgXRHS9f8Sgx40m7kxpejddG", - }; - - const expectedUser = { - username: "admin", - password: - "$2b$10$gKoU.xdV9vrGY2wEW0KAnuBmQeYxOUgXRHS9f8Sgx40m7kxpejddG", - }; - - await changePassword(newUserPasswordData); - let user = await getUser("admin"); - //console.info(user); - expect(user).toMatchObject(expectedUser); - } catch (error) { - console.error(error); - } - }); }); diff --git a/components/colorform.js b/components/colorform.js index d44b1b7..8afefdd 100644 --- a/components/colorform.js +++ b/components/colorform.js @@ -1,15 +1,57 @@ +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import styles from "../styles/form.module.css"; const ColorForm = ({ data, update, loading }) => { - const { register, handleSubmit } = useForm({ + const [isGradeints, setisGradeints] = useState(false); + const [colorWiseGradeints, setcolorWiseGradeints] = useState({}); + + useEffect(() => { + //create an object to store color state of each input (gradient or normal(hex)) + let colorWiseGradientState = { + bgColor: data.bgColor, + accentColor: data.accentColor, + handlerFontColor: data.handlerFontColor, + footerBgColor: data.footerBgColor, + footerTextColor: data.footerTextColor, + avatarBorderColor: data.avatarBorderColor, + handlerDescriptionFontColor: data.handlerDescriptionFontColor, + }; + + // check whether the current value is gradient or hex + for (const [key, value] of Object.entries(colorWiseGradientState)) { + if (!String(value).startsWith("#")) { + colorWiseGradientState[key] = true; + setisGradeints(true); + } else { + colorWiseGradientState[key] = false; + } + } + setcolorWiseGradeints(colorWiseGradientState); + }, [data]); + + // reset the form when the gradeint switch state change + useEffect(() => { + reset({ + bgColor: data.bgColor, + accentColor: data.accentColor, + handlerFontColor: data.handlerFontColor, + footerBgColor: data.footerBgColor, + footerTextColor: data.footerTextColor, + avatarBorderColor: data.avatarBorderColor, + handlerDescriptionFontColor: data.handlerDescriptionFontColor, + }); + }, [isGradeints]); + + const { register, handleSubmit, reset } = useForm({ defaultValues: { bgColor: data.bgColor, accentColor: data.accentColor, handlerFontColor: data.handlerFontColor, footerBgColor: data.footerBgColor, footerTextColor: data.footerTextColor, + avatarBorderColor: data.avatarBorderColor, handlerDescriptionFontColor: data.handlerDescriptionFontColor, }, }); @@ -20,16 +62,37 @@ const ColorForm = ({ data, update, loading }) => {
+ {" "} +

Colors

+
+
+ + { + setisGradeints(!isGradeints); + }} + /> +
+
e.preventDefault()}> -

Colors

-
- - +
+ @@ -37,10 +100,16 @@ const ColorForm = ({ data, update, loading }) => {
- + @@ -51,10 +120,18 @@ const ColorForm = ({ data, update, loading }) => {
- + @@ -62,10 +139,18 @@ const ColorForm = ({ data, update, loading }) => {
- + @@ -75,27 +160,61 @@ const ColorForm = ({ data, update, loading }) => {
- + +
{" "}
-
- +
+
{" "}
+
+
+
+ + +
{" "} +
+
{" "} + {" "} {/*
- {activeForm === "genaralForm" && ( - )} {activeForm === "linksForm" && } + {activeForm === "socialForm" && ( + + )} {activeForm === "passwordchangeform" && }
{ +const GeneralForm = ({ data, update, loading }) => { const { register, handleSubmit, @@ -70,7 +70,58 @@ const GenaralForm = ({ data, update, loading }) => { rows="3" {...register("handlerDescription")} > -
+
{" "} +
+ +
+ {" "} + px{" "} + {errors.linktreeWidth && ( +
+ {errors.linktreeWidth.message} +
+ )} +
+
{" "} +
+ +
+ {" "} + em{" "} + {errors.linkPadding && ( +
+ {errors.linkPadding.message} +
+ )} +
+
{" "}
@@ -199,4 +250,4 @@ const GenaralForm = ({ data, update, loading }) => { ); }; -export default GenaralForm; +export default GeneralForm; diff --git a/components/linkcard.js b/components/linkcard.js index 8f6ba5e..1af7a62 100644 --- a/components/linkcard.js +++ b/components/linkcard.js @@ -1,9 +1,18 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; - +import { Draggable } from "react-beautiful-dnd"; import debounce from "lodash.debounce"; -export default function LinkCard({ item, updateLink, deleteLink, loading }) { +import styles from "../styles/utils.module.css"; + +export default function LinkCard({ + item, + updateLink, + deleteLink, + loading, + index, + isDragDisabled, +}) { const refSubmitButtom = useRef(null); const [cardInfo, setCardInfo] = useState(item); @@ -28,6 +37,17 @@ export default function LinkCard({ item, updateLink, deleteLink, loading }) { reset(item); setCardInfo(item); } + // logic to test comment settings data and reset + // reset(item); + if ( + cardInfo.borderRadius !== item.borderRadius || + cardInfo.textColor !== item.textColor || + cardInfo.bgColor !== item.bgColor + ) { + // console.log("reset after common settings change"); + reset(item); + setCardInfo(item); + } }, [item]); watch((data, { type }) => { @@ -57,165 +77,185 @@ export default function LinkCard({ item, updateLink, deleteLink, loading }) { return ( <> -
-
- {/* {console.log(errors)} */} - {/* {JSON.stringify(item)} */} - -
- -
-
- {/* */} - - {errors.displayText && ( -
- Link Display Text is required -
- )} -
-
- {/* */} - - {errors.linkUrl && ( -
{errors.linkUrl.message}
- )} -
-
- {/* */} -
- Use{" "} - - fontawesome - {" "} - for icon classes -
-
- -
- {/* */} -
- - px - {errors.borderRadius && ( -
- {errors.borderRadius.message} + + {(provided) => ( +
+
+
+ +
+
{" "} +
+ +
+
+
+ + {errors.displayText && ( +
+ Link Display Text is required +
+ )} +
+
+ + {errors.linkUrl && ( +
+ {errors.linkUrl.message} +
+ )} +
+
+
+ Use{" "} + + fontawesome + {" "} + for icon classes +
- )} -
-
-
-
-
- -
-
-
-
- +
+
+ + px + {errors.borderRadius && ( +
+ {errors.borderRadius.message} +
+ )} +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+ {/*
+ -
+
*/} +
+ +
+
- {/*
- - -
*/} -
- -
-
-
+
+ )} + ); } diff --git a/components/linksform.js b/components/linksform.js index 02b09f7..47833db 100644 --- a/components/linksform.js +++ b/components/linksform.js @@ -1,9 +1,13 @@ -import styles from "../styles/form.module.css"; -import LinkCard from "./linkcard"; -import { useStateValue } from "./context/state"; import { useState } from "react"; -import Swal from "sweetalert2"; import { ToastContainer, toast } from "react-toastify"; +import Swal from "sweetalert2"; +import { DragDropContext, Droppable } from "react-beautiful-dnd"; + +import { useStateValue } from "./context/state"; +import LinkCard from "./linkcard"; + +import styles from "../styles/form.module.css"; +import { useForm } from "react-hook-form"; const endpoint = process.env.NODE_ENV === "production" ? `` : "http://localhost:3000"; @@ -11,10 +15,18 @@ const endpoint = const LinksForm = ({ pagedataid }) => { const [{ links }, dispatch] = useStateValue(); const [loading, setloading] = useState(false); + const [commonSettingsCollapse, setcommonSettingsCollapse] = useState(false); + const [isNewLinkInList, setisNewLinkInList] = useState(false); + const { + register, + handleSubmit, + formState: { errors, dirtyFields }, + } = useForm(); const addNewLink = () => { // console.log(links.length); // console.log(links[links.length - 1]); + setisNewLinkInList(true); let newLink = links[links.length - 1]; @@ -44,13 +56,18 @@ const LinksForm = ({ pagedataid }) => { // console.log(linkdata); setloading(true); - let operation = "insertpagelinks"; + let operation = "insertlinks"; if (linkdata.hasOwnProperty("id")) { - operation = `updatepagelinks`; + operation = `updatelinks`; + } + + if (operation === "insertlinks") { + setisNewLinkInList(false); } + // console.log(operation); try { - let res = await fetch(`${endpoint}/api/${operation}`, { + let res = await fetch(`${endpoint}/api/pages/${operation}`, { method: "POST", body: JSON.stringify(linkdata), headers: { "Content-Type": "application/json" }, @@ -59,15 +76,7 @@ const LinksForm = ({ pagedataid }) => { // console.log(res); if (!res.success) { - toast.error(`Error ${res.message}`, { - position: "bottom-left", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - }); + toast.error(`Error ${res.message}`, { autoClose: 5000 }); setloading(false); return; } @@ -75,31 +84,15 @@ const LinksForm = ({ pagedataid }) => { dispatch({ type: "updateLink", linkdata: res.updatedLinkData }); toast.success( `${ - operation === "insertpagelinks" + operation === "insertlinks" ? "Added new page link " : "Updated page link " + " successfully" }`, - { - position: "bottom-left", - autoClose: 1000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - } + { autoClose: 1000 } ); } catch (error) { console.log(error); - toast.error(`Error : ${error.message}`, { - position: "bottom-left", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - }); + toast.error(`Error : ${error.message}`, { autoClose: 5000 }); } setloading(false); }; @@ -123,7 +116,7 @@ const LinksForm = ({ pagedataid }) => { setloading(true); try { - let res = await fetch(`${endpoint}/api/deletepagelink`, { + let res = await fetch(`${endpoint}/api/pages/deletelink`, { method: "POST", body: JSON.stringify({ id: id }), headers: { "Content-Type": "application/json" }, @@ -132,39 +125,129 @@ const LinksForm = ({ pagedataid }) => { console.log(res); if (!res.success) { - toast.error(`Error ${res.message}`, { - position: "bottom-left", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - }); + toast.error(`Error ${res.message}`, { autoClose: 5000 }); setloading(false); return; } dispatch({ type: "deleteLink", id: id }); - toast.success(`successfully deleted link`, { - position: "bottom-left", + toast.success(`Successfully deleted link`, { autoClose: 1000 }); + } catch (error) { + console.log(error); + toast.error(`Error : ${error.message}`, { autoClose: 5000 }); + } + setloading(false); + }; + + const dragEndHnadler = async (data) => { + // console.log(data); + if (!data.destination) { + return; + } + + if (data.destination.index === data.source.index) { + return; + } + + setloading(true); + + const items = Array.from(links); + const [reorderedItem] = items.splice(data.source.index, 1); + items.splice(data.destination.index, 0, reorderedItem); + + let updateditems = items.map((item, index) => { + item.orderIndex = index; + return item; + }); + + dispatch({ type: "updateLink", linkdata: updateditems }); + + let orderData = updateditems.map((item) => { + return { + id: item.id, + name: item.displayText, + orderIndex: item.orderIndex, + }; + }); + + // console.log(orderData); + + try { + let res = await fetch(`${endpoint}/api/pages/reorderlinks`, { + method: "POST", + body: JSON.stringify({ orderData }), + headers: { "Content-Type": "application/json" }, + }).then((res) => res.json()); + + if (!res.success) { + toast.error(`Error ${res.message}`, { autoClose: 5000 }); + return; + } + + toast.success(`Successfully reordered links`, { autoClose: 1000 }); + setloading(false); + } catch (error) { + console.log(error); + setloading(false); + toast.error(`Error : ${error.message}`, { autoClose: 5000 }); + } + }; + + const updateCommonSettings = async (data) => { + // console.log(data); + // console.log(dirtyFields); + // only get user changed data + + if (Object.keys(dirtyFields).length === 0) { + return; + } + + Object.keys(data).forEach((element) => { + if (!dirtyFields[element]) { + delete data[element]; + } + }); + + console.log(data); + + let confirm = await Swal.fire({ + title: "Apply common settings", + text: "All your links will apply these common settings", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, apply it!", + }); + + if (!confirm.isConfirmed) { + return; + } + + setloading(true); + + try { + let res = await fetch(`${endpoint}/api/pages/applycommonsettigns`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }).then((res) => res.json()); + + console.log(res); + + if (!res.success) { + toast.error(`Error ${res.message}`, { autoClose: 5000 }); + setloading(false); + return; + } + + dispatch({ type: "updateLink", linkdata: res.updatedLinkData }); + + toast.success(`Successfully applied common settings`, { autoClose: 1000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, }); } catch (error) { console.log(error); - toast.error(`Error : ${error.message}`, { - position: "bottom-left", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - }); + toast.error(`Error : ${error.message}`, { autoClose: 5000 }); } setloading(false); }; @@ -194,22 +277,137 @@ const LinksForm = ({ pagedataid }) => { > Add new link - {links && - links.map((item, index) => { - return ( - - ); - })} +
+
+
+
+

+ +

{" "} +
+
+
+ {" "} +
+
+ + px + {errors.borderRadius && ( +
+ {errors.borderRadius.message} +
+ )} +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
{" "} + +
+
+
+
+
+
+
+ + + {(provided) => ( +
+ {links.length > 0 && + links.map((item, index) => { + return ( + + ); + })} + {provided.placeholder} +
+ )} +
+
{ - // return ` - // .link-${id} { - // background-color: ${ele.bgColor}; - // color: ${ele.textColor || "#ffffff"}; - // }`; - // }) - // .join() - // ); + linkPadding = isEmpty(linkPadding) ? "2em" : `${linkPadding}em`; + linktreeWidth = isEmpty(linktreeWidth) ? "320px" : `${linktreeWidth}px`; return (
@@ -54,18 +53,46 @@ export default function Home({
{!isEmpty(avatarUrl) && } - - {handlerText} - + + + {handlerText}{" "} + + +

{handlerDescription}

+
+ +
    - {/* {[...linkData, ...linkData, ...linkData].map((element, id) => { */} {linkData.map((link, id) => { return (
  • @@ -92,13 +119,13 @@ export default function Home({
+ {footerEnabled && ( +
+ {/* Copyright Β© 2021 All Rights Reserved by. */} + {footerText} +
+ )}
- {footerEnabled && ( -
- {/* Copyright Β© 2021 All Rights Reserved by. */} - {footerText} -
- )}