text:
import { Companies, CompanyType, Island, SocialNetworkType } from "./models.ts";

export const companies: Companies = [
  {
    name: "Acid Tango",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://acidtango.com/?utm_source=github&utm_medium=readme-list&utm_campaign=github-tech-companies-canarias&utm_content=github-daniel-ramos-tech-companies-canarias",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/Acid-Tango-101103077972357",
      },
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/acidtango",
      },
      {
        type: SocialNetworkType.INSTAGRAM,
        link: "https://www.instagram.com/acidtango",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/acid-tango",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/acidtango",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/channel/UCFqkwvYiCRR3dupcxvbNOGA",
      },
    ],
    jobPage:
      "https://www.linkedin.com/company/acid-tango/jobs/",
  },
  {
    name: "Arte Consultores",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://arte.dev/index-es.html",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/artedev",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/arte_dev",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/channel/UCAEdAn0If7IN_xVBgrZftaw",
      },
    ],
    jobPage:
      "https://arte.dev/trabajo.html",
  },
  {
    name: "Avantic",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "http://www.avantic.net",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/avantic",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/avantic-estudio-de-ingenieros",
      },
    ],
    jobPage:
      "https://www.linkedin.com/company/avantic-estudio-de-ingenieros/jobs",
  },
  {
    name: "Conectatec",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.conectatec.com/?utm_source=github",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/conectatectf",
      },
      {
        type: SocialNetworkType.INSTAGRAM,
        link: "https://www.instagram.com/conectatec",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/conectatec",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/user/seoconectatec",
      },
    ],
    jobPage:
      "https://www.linkedin.com/company/conectatec/jobs",
  },
  {
    name: "Desarrollo Cometa",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.desarrollocometa.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/desarrollocometa",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/devCometa",
      },
    ],
    jobPage: "https://www.desarrollocometa.com",
  },
  {
    name: "Ecommium",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://ecommium.com",
    hiringFrom: [Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/ecommium-ecommerce",
      },
    ],
    jobPage: "https://ecommium.com/unete-al-equipo",
  },
  {
    name: "edataconsulting",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://edataconsulting.es",
    hiringFrom: [Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/edataconsulting",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/edataconsulting/",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/edataconsulting",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/channel/UCEEixyMsLJPw7-v_gtkSDrw",
      },
    ],
    jobPage: "https://people.edataconsulting.es/",
  },
  {
    name: "Expero",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.experoinc.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/experoinc",
      },
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/experoinc",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/experoinc",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/experoinc",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/channel/UCSpX7JV7sC-mH46oM09n_Nw",
      },
    ],
    jobPage: "https://www.experoinc.com/about/careers",
  },
  {
    name: "Glotio",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://glotio.com",
    hiringFrom: [Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/glotio",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/Glotio1",
      },
    ],
    jobPage: "https://www.linkedin.com/company/glotio/jobs",
  },
  {
    name: "HolaSoft",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.holasoft.es",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/HolaSoft.tpv.erp",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/holasoft",
      },
    ],
    jobPage: "https://www.linkedin.com/company/holasoft/jobs",
  },
  {
    name: "HYVE",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.hyve.net",
    hiringFrom: [Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/hyvetheinnovationcompany",
      },
      {
        type: SocialNetworkType.INSTAGRAM,
        link: "https://www.instagram.com/hyve_innovation",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/hyve-ag",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/hyve_innovation",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/channel/UCnazwh6SE2Rex_J2982kyTQ",
      },
    ],
    jobPage: "https://www.hyve.net/en/careers",
  },
  {
    name: "Ingenium Ventures",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.ingeniumventures.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/IngeniumVentures",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/ingenium-ventures",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/ingeniumv",
      },
    ],
    jobPage: "https://www.ingeniumventures.com/talento",
  },
  {
    name: "Intelequia",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://intelequia.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/Intelequia",
      },
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/intelequia",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/intelequia",
      },
    ],
    jobPage: "https://intelequia.com/social/empleos",
  },
  {
    name: "Lean Mind",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://leanmind.es",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/Lean-Mind-106484421092863",
      },
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/lean-mind",
      },
      {
        type: SocialNetworkType.INSTAGRAM,
        link: "https://www.instagram.com/leanfulness_es",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/leanfulness",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/leanfulness_es",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/channel/UCW16ot2jRfF3LgUSCEiGd8g",
      },
    ],
    jobPage: "https://leanmind.es/es/empleo",
  },
  {
    name: "MHP Software",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "http://mhp-solution-group.es/",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/mhpes",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/mhp-software-s-l-/",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/mhpsoftware",
      },
    ],
    jobPage: "https://www.infojobs.net/mhp-software-sl/em-i98555454495549777280322013095644317473/ofertas",
  },
  {
    name: "Omnia Infosys",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://omniainfosys.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/Omnia-Infosys-126364607419620",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/omnia-infosys-slu",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/OmniaInfosys",
      },
    ],
    jobPage: "https://www.linkedin.com/company/omnia-infosys-slu/jobs/",
  },
  {
    name: "Open Canarias",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.opencanarias.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/opencanarias",
      },
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/opencanarias",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/open-canarias-sl",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/OpenCanarias",
      },
    ],
    jobPage: "https://www.linkedin.com/company/open-canarias-sl/jobs",
  },
  {
    name: "Prodanet",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.prodanet.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/prodanet",
      },
    ],
    jobPage: "https://www.linkedin.com/company/prodanet/jobs",
  },
  {
    name: "Proyectran",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.proyectran.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/Proyectran",
      },

      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/proyectran",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/Proyectran",
      },
    ],
    jobPage: "https://www.linkedin.com/company/proyectran/jobs",
  },
  {
    name: "Secret Source",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.secret-source.eu",
    hiringFrom: [Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/secretsource",
      },
      {
        type: SocialNetworkType.INSTAGRAM,
        link: "https://www.instagram.com/secret_source.eu",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/secret-source",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/SecretSourceEU",
      },
    ],
    jobPage: "https://careers.secret-source.eu",
  },
  {
    name: "Sirokko Open Source Solutions",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.sirokko.es",
    hiringFrom: [Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/SirokkoOSS",
      },
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/sirokko",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link:
          "https://www.linkedin.com/company/sirokko-open-source-solutions-s-l-/",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/sirokkooss",
      },
    ],
    jobPage:
      "https://www.linkedin.com/company/sirokko-open-source-solutions-s-l-/jobs/",
  },
  {
    name: "Squaads",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://squaads.com",
    hiringFrom: [Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/squaads/",
      },
    ],
    jobPage: "https://www.linkedin.com/company/squaads/jobs/",
  },
  {
    name: "System73 Ltd.",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://system73.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/system73limited",
      },
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/system73",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/system-73",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/system73limited",
      },
    ],
    jobPage: "https://system73.com/careers",
  },
  {
    name: "TecAlliance",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.tecalliance.net",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "facebook.com/TecAlliance",
      },
      {
        type: SocialNetworkType.INSTAGRAM,
        link: "https://www.instagram.com/tecalliance",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/tecalliance",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/TecAlliance",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/channel/UC_b7gPw0OabQ3b2CDTtdFnw",
      },
    ],
    jobPage: "https://www.tecalliance.net/en/career",
  },
  {
    name: "The Agile Monkeys",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.theagilemonkeys.com",
    hiringFrom: [Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.FACEBOOK,
        link: "https://www.facebook.com/TheAgileMonkeys",
      },
      {
        type: SocialNetworkType.GITHUB,
        link: "https://github.com/theam",
      },
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/theagilemonkeys",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/theagilemonkeys",
      },
    ],
    jobPage: "https://www.linkedin.com/company/theagilemonkeys/jobs",
  },
  {
    name: "Técnicas Competitivas",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.tecnicascompetitivas.com",
    hiringFrom: [Island.TENERIFE, Island.GRAN_CANARIA],
    socialNetworks: [
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/tecnicas-competitivas",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/TecnicasTCSA",
      },
    ],
    jobPage: "https://www.linkedin.com/company/tecnicas-competitivas/jobs",
  },
  {
    name: "Ultebra",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.ultebra.eu",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/ultebra-solutions-s-l",
      },
      {
        type: SocialNetworkType.TWITTER,
        link: "https://twitter.com/ultebra",
      },
    ],
    jobPage: "https://www.ultebra.eu/#join",
  },
  {
    name: "XalDigital",
    type: CompanyType.SOFTWARE_DEVELOPMENT,
    website: "https://www.xaldigital.com",
    hiringFrom: [Island.TENERIFE],
    socialNetworks: [
      {
        type: SocialNetworkType.LINKEDIN,
        link: "https://www.linkedin.com/company/xal-digital",
      },
      {
        type: SocialNetworkType.YOUTUBE,
        link: "https://www.youtube.com/channel/UCmC9ZbHwg_5Jff9QNRoRofw",
      },
    ],
    jobPage: "https://www.linkedin.com/company/xal-digital/jobs",
  },
];


text:
import { companies } from "../companies.ts";
import { companiesAreSortedAlphabetically } from "./rules/companiesAreSortedAlphabetically.ts";
import { socialNetworksAreSortedAlphabetically } from "./rules/socialNetworksAreSortedAlphabetically.ts";

function ensure(error?: Error) {
  if (error) {
    throw error;
  }
}

ensure(companiesAreSortedAlphabetically(companies));
ensure(socialNetworksAreSortedAlphabetically(companies));

console.log("All ok!");

repo_language: TypeScript

mime_type: text/plain


text:
import { Companies } from "../../models.ts";

export function socialNetworksAreSortedAlphabetically(companies: Companies) {
  for (const company of companies) {
    const socialNetworks = company.socialNetworks.map((socialNetwork) =>
      socialNetwork.type
    );
    const sortedSocialNetworks = [...socialNetworks].sort();

    for (let i = 0; i < socialNetworks.length; i++) {
      const socialNetwork = socialNetworks[i];
      const expectedCompanyName = sortedSocialNetworks[i];

      if (socialNetwork !== expectedCompanyName) {
        return new Error(
          `Social networks of company '${company.name}' are not sorted`,
        );
      }
    }
  }
}


text:
import { Companies } from "../../models.ts";

export function companiesAreSortedAlphabetically(companies: Companies) {
  const companyNames = companies.map((company) => company.name.toLowerCase());
  const sortedCompanyNames = [...companyNames].sort();

  for (let i = 0; i < companyNames.length; i++) {
    const companyName = companyNames[i];
    const expectedCompanyName = sortedCompanyNames[i];

    if (companyName != expectedCompanyName) {
      return new Error(
        `Companies are not sorted, expected '${expectedCompanyName}' but found '${companyName}'`,
      );
    }
  }
}


text:
import { expect } from "../../../deps.ts";
import { createCompany } from "../../../test/createCompany.ts";
import { SocialNetworkType } from "../../models.ts";
import { socialNetworksAreSortedAlphabetically } from "./socialNetworksAreSortedAlphabetically.ts";

Deno.test({
  name: "returns void if social networks are sorted",
  fn: () => {
    const companies = [
      createCompany({
        socialNetworks: [
          {
            type: SocialNetworkType.INSTAGRAM,
            link: "https://www.instagram.com/example",
          },
          {
            type: SocialNetworkType.LINKEDIN,
            link: "https://www.linkedin.com/company/example",
          },
        ],
      }),
    ];

    const result = socialNetworksAreSortedAlphabetically(companies);

    expect(result).toBeUndefined();
  },
});

Deno.test({
  name: "returns an error if social networks are not sorted",
  fn: () => {
    let companyName = "My Company";
    const companies = [
      createCompany({
        name: companyName,
        socialNetworks: [
          {
            type: SocialNetworkType.LINKEDIN,
            link: "https://www.linkedin.com/company/example",
          },
          {
            type: SocialNetworkType.INSTAGRAM,
            link: "https://www.instagram.com/example",
          },
        ],
      }),
    ];

    const result = socialNetworksAreSortedAlphabetically(companies) as Error;

    expect(result).toBeInstanceOf(Error);
    expect(result.message).toEqual(
      `Social networks of company '${companyName}' are not sorted`,
    );
  },
});


text:
import { expect } from "../../../deps.ts";
import { createCompany } from "../../../test/createCompany.ts";
import { companiesAreSortedAlphabetically } from "./companiesAreSortedAlphabetically.ts";

Deno.test({
  name: "returns void if commpanies are sorted",
  fn: () => {
    const companies = [
      createCompany({ name: "A Company" }),
      createCompany({ name: "B Company" }),
    ];

    const result = companiesAreSortedAlphabetically(companies);

    expect(result).toBeUndefined();
  },
});

Deno.test({
  name: "returns an error if companies are not sorted",
  fn: () => {
    const companies = [
      createCompany({ name: "B Company" }),
      createCompany({ name: "A Company" }),
    ];

    const result = companiesAreSortedAlphabetically(companies) as Error;

    expect(result).toBeInstanceOf(Error);
    expect(result.message).toEqual(
      "Companies are not sorted, expected 'a company' but found 'b company'",
    );
  },
});

Deno.test({
  name: "returns an error if companies are not sorted ignoring the case",
  fn: () => {
    const companies = [
      createCompany({ name: "a Company" }),
      createCompany({ name: "B Company" }),
    ];

    const result = companiesAreSortedAlphabetically(companies) as Error;

    expect(result).toBeUndefined();
  },
});


text:
import {
  Company,
  CompanyType,
  Island,
  SocialNetworkType,
} from "../src/models.ts";

export const createCompany = ({
  name = "Example Company",
  type = CompanyType.SOFTWARE_DEVELOPMENT,
  website = "https://example.org/",
  hiringFrom = [Island.TENERIFE],
  socialNetworks = [{
    type: SocialNetworkType.INSTAGRAM,
    link: "https://www.instagram.com/example",
  }],
  jobPage = "https://example.org/jobs",
} = {}): Company => ({
  name,
  type,
  website,
  hiringFrom,
  socialNetworks,
  jobPage,
});


text:
{:deps     {:aliases [:dev]}
 :http     {:port 9631} ; shadow control server
 :nrepl    {:port 9001}
 :dev-http {8000 "resources/public"}
 :jvm-opts ["-Xmx2G"]
 :builds   {:main     {:target     :browser
                       :output-dir "resources/public/js/main"
                       :asset-path "/js/main"
                       :dev        {:compiler-options {:external-config {:guardrails {:emit-spec? true :throw? false}}}}
                       :release    {:compiler-options {:source-map true}}
                       :modules    {:main {:init-fn holyjak.fulcro-exercises/init}}
                       :devtools   {:preloads   [shadow.remote.runtime.cljs.browser ; Shadow Inspect tab support
                                                 com.fulcrologic.fulcro.inspect.preload
                                                 com.fulcrologic.fulcro.inspect.dom-picker-preload]
                                    :after-load holyjak.fulcro-exercises.impl/refresh}}
            :puzzles {:target     nubank.workspaces.shadow-cljs.target
                      :ns-regexp  "-(test|ws)$"
                      :output-dir "resources/public/workspaces/js"
                      :asset-path "/workspaces/js"
                      :devtools   {:preloads           [com.fulcrologic.fulcro.inspect.preload]
                                   :http-root          "resources/public/workspaces"
                                   :http-port          8023
                                   :http-resource-root "."}}}}

repo_name: fulcro-community/fulcro-exercises

repo_language: Clojure


text:
{:paths ["src" "resources"]
 :deps {org.clojure/clojure    {:mvn/version "1.10.3"}
        com.fulcrologic/fulcro {:mvn/version "3.5.2"}
        com.wsscode/pathom     {:mvn/version "2.3.1"}}
 :aliases
 {:dev   {:extra-deps {thheller/shadow-cljs {:mvn/version "2.15.5"}
                       binaryage/devtools   {:mvn/version "1.0.0"}
                       ;; for Workspaces:
                       com.github.awkay/workspaces {:mvn/version "1.0.2"}
                       ;; Emacs requirements for M-x cider-connect-cljs:
                       cider/cider-nrepl {:mvn/version "0.26.0"}
                       ;; FIXME: Do we really need piggieback for Emacs?!
                       cider/piggieback {:mvn/version "0.5.1"}}}
;; NOTE: Running shadow via clojure -M:... does not work well with Calva's Jack-in, since it wants to do
;;       its own main opts
;;   :serve {:jvm-opts    ["-XX:-OmitStackTraceInFastThrow"]
;;           :main-opts ["-m" "shadow.cljs.devtools.cli" "watch" "main"]
;;           :extra-deps {thheller/shadow-cljs                {:mvn/version "2.14.5"}
;;                        binaryage/devtools                  {:mvn/version "1.0.0"}}}
;;   ;; Activate if you want to `M-x cider-connect-cljs` from Emacs:
;;   :cider {:extra-deps {cider/cider-nrepl {:mvn/version "0.25.9"}           ; must be added for M-x cider-connect-cljs
;;                        cider/piggieback {:mvn/version "0.5.1"}}}

;;   :serve-puzzles {:jvm-opts    ["-XX:-OmitStackTraceInFastThrow"]
;;                   :main-opts ["-m" "shadow.cljs.devtools.cli" "watch" "workspaces"]
;;                   :extra-deps {thheller/shadow-cljs                {:mvn/version "2.14.5"}
;;                                binaryage/devtools                  {:mvn/version "1.0.0"}
;;                                ;; for Workspaces:
;;                                com.github.awkay/workspaces {:mvn/version "1.0.2"}}}}
  }}


text:
;; OPTIONAL - for running the app via babashka tasks instead of manually
{:tasks
 {:requires ([babashka.fs :as fs])
  npm-inst (when-not (fs/directory? "node_modules")
             (shell "npm install"))
  run {:depends [npm-inst], :task (shell "npx shadow-cljs watch main")}
  run-puzzles {:depends [npm-inst], :task (shell "npx shadow-cljs watch puzzles")}
  ;; for backwards compatibility:
  serve {:depends [run]}
  serve-puzzles {:depends [run-puzzles]}}}


text:
# fulcro-exercises

A follow-up to the [Minimalist Fulcro Tutorial](https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/) to get hands dirty with Fulcro and learn and experience the theory in practice, through a series of gradually more challenging exercises.

## Prerequisites

* Studied the Minimalist Fulcro Tutorial
* Familiar with Clojure, ClojureScript, and at least somewhat with shadow-cljs

## Feedback!

Please share your experiences and ideas with me so that I can improve the exercises! Get in touch with `@holyjak` in the [`#fulcro` channel of the Clojurians Slack](https://app.slack.com/client/T03RZGPFR/C68M60S4F) or in [Zulip](https://clojurians.zulipchat.com/) or by [starting a discussion](https://github.com/fulcro-community/fulcro-exercises/discussions) here at GitHub.

## Usage

Prerequisites: same as the [shadow-cljs requirements](https://github.com/thheller/shadow-cljs#requirements).

Clone this repo, enter the directory. Then install prerequisites:

    npm install
    # or: yarn install  # reportedly yarn < v3 as of 8/2021

### Part 1: The exercises

Compile the ClojureScript code and start a server for the UI:

    npx shadow-cljs watch main
    # or `bb run` if you have Babashka

NOTE: For Calva, do instead start the REPL from the editor - [run Jack-in](https://calva.io/connect/#jack-in-let-calva-start-the-repl-for-you), selecting _shadow-cljs_ then the `:main` build. Remember to load the page in the browser, see below.

NOTE: For Cursive, in Run/Debug Configurations go to: Add new configuration (+) → Clojure REPL → Remote → Connection type: nREPL | Connection details: Connect to server | Host: localhost | Port: 9001 (Make sure to set the right context module as sometimes it is shown empty)

NOTE: For Emacs, connect to the REPL using `M-x cider-connect-cljs`

**BEWARE**: It might not work if your version of the [Clojure CLI](https://clojure.org/guides/getting_started#_clojure_installer_and_cli_tools) (`clojure --version`) is too old or, possibly, too new. The project has been tested with v.1.10.3.855.

You can now see the application at http://localhost:8000. Open it in Chrome, where you have [installed Fulcro Inspect](https://book.fulcrologic.com/#_install_fulcro_inspect) and [enabled custom formatters](https://book.fulcrologic.com/#_configure_chrome_development_settings)!

Now (after having accessed the web page!) connect to the cljs REPL: first connect to the nREPL running at port 9001 then execute `(shadow/repl :main)` there. (If you get "_No available JS runtime._" then you likely forgot to load the page in a browser first.)

Finally, open the `holyjak.fulcro-exercises` namespace and get coding, following the instructions provided there in the ns docstring!

**TIP**: Make sure that you select the correct _App_ at the bottom of Fulcro Inspect, if there are multiple (or hard-reload the page to get rid of all the past ones).

#### Troubleshooting and getting help during the exercises

Use repeatedly `(hint <exercise number>)` (as long as there are any more hints for the exercise) to get useful tips when you get stuck.

Leverage the [Fulcro Troubleshooting Decision Tree](https://blog.jakubholy.net/2020/troubleshooting-fulcro/) to help you troubleshoot your problems.

Leverage Fulcro Inspect (especially the DB and perhaps Element tabs), check the Chrome JS Console for warnings and errors.

#### Various problems and solutions

##### Fulcro Inspect shows an empty client DB

It might help to close it, reload the page, then open it again.

##### 'shadow-cljs - (reload failed)' reported in the browser

If shadow-cljs fails because of a coding error then this error is reported in the browser. It may break live reloading, i.e. you might need to hard-reload the page after you fix it.

### Part 2: Fulcro Puzzles

After you have completed the exercises, you can check you insight and troubleshooting skill by fixing the broken pieces of code presented in the `holyjak.fulcro-exercises.puzzles.puzzles-ws` namespace.

To run the puzzles, stop the exercises process and run instead this:

    npx shadow-cljs watch puzzles
    # or `bb run-puzzles` if you have Babashka

NOTE: For Calva, do instead start the REPL from the editor - [run Jack-in](https://calva.io/connect/#jack-in-let-calva-start-the-repl-for-you), selecting _shadow-cljs_ then the `:puzzles` build.

Then navigate to http://localhost:8023/ and in the left-side menu, under _Cards_, select the first puzzle card.

Try to fix the code so that the card does what it is supposed to. Remember to use primarily the Fulcro Database and Fulcro
Inspector for the troubleshooting! If you get stuck or after you finish your solution,
compare it to the solution in the `holyjak.fulcro-exercises.puzzles.solutions-ws` namespace.

## License

Copyleft © 2021 Jakub Holý

Distributed under the [Unlicense](https://unlicense.org/).


text:
(ns holyjak.fulcro-exercises
  "This is a series of exercises to help you learn Fulcro, via the REPL. See the README.md!

  How to use:
  - Load this file into the cljs REPL (using the commands provided by your editor)
  - For each exercise:
    1. Un-comment *temporarily* the exercise you are working on (by replacing `(comment ...`
      with `(do ;comment ...`) Shadow-cljs will thus evaluate the code and it will render.
      Tip: Hard-reload the page occasionally to get rid of residues of past exercises.
    2. Read the instructions in the comments, make your changes, wait for shadow to apply them
    3. Regularly look at what your solution looks like in Fulcro Inspect (Client DB, ...) to
       get familiar with this essential tool.
    4. Call `(hint <exercises number>)` to get help. Repeated calls may provide more help.
       Even if you want to do it without help, check out all the hints eventually. They may
       contain useful insights.
    5. When done, compare your solution with mine in the `holyjak.solutions` namespace.
    6. Finally, comment-out the exercise again and go on to the next one.
  - See 'Troubleshooting and getting help during the exercises' in the README
  "
  (:require
    [holyjak.fulcro-exercises.impl :refer [hint config-and-render! show-client-db]]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]
    [com.fulcrologic.fulcro.algorithms.data-targeting :as targeting]
    [com.fulcrologic.fulcro.algorithms.normalized-state :as norm]
    [com.fulcrologic.fulcro.application :as app]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc transact!]]
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.fulcrologic.fulcro.mutations :refer [defmutation]]
    [com.fulcrologic.fulcro.dom :as dom :refer [button div form h1 h2 h3 input label li ol p ul]]
    [com.wsscode.pathom.connect :as pc :refer [defresolver]]
    [com.fulcrologic.fulcro.mutations :as m]))

(defn init [])

;; ### TODO: Read the namespace docstring for instructions how to work with the exercises ###

(do; comment
  (do
    ;; TASK 0.0: Comment this out by replacing the `(do; comment` above with `(comment` and go on to the next exercise.
    ;; LEARNING OBJECTIVES: Get familiar with switching from an exercise to another.
    (defsc Root00 [_ _]
      (div
        (h1 "Welcome to Fulcro exercises!")
        (p "This is an example what an exercise looks like while you work on it.")
        (p "Just comment this exercises out as describe in the code comment above and go on to the next.")))

    (config-and-render! Root00)
    ,))

(comment ; 0 "Try it out!"
  (do
    ;; LEARNING OBJECTIVES: Get familiar with switching to a new exercise and using the hints.
    (defsc Root0 [_ _]
      (h1 "Hello, I am a Fulcro app from the exercise 0!"))

    (config-and-render! Root0)

    (comment ; try running the hint fn 3 times!
      (hint 0)
      (hint 0)
      (hint 0))

    nil))

(comment ; 1 "Hard-coded DOM"
  (do
    ;; TASK:
    ;; Complete the body of the Root1 component to render the following html:
    ;;
    ;;

    ;;
Fulcro is:

    ;;

    ;;
Malleable

    ;;
Full-stack

    ;;
Well-designed

    ;;

    ;;

    ;;
    ;; LEARNING OBJECTIVES: Get familiar with creating elements and passing in props.
    ;;
    ;; RESOURCES:
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_rendering_dom_the_body_of_a_component
    ;; - https://reactjs.org/docs/dom-elements.html#style
    (defsc Root1 [_ _]
      {}
      "TODO")

    (config-and-render! Root1)))

(comment ; 2 "Extracting a child component"
  (do
    ;; TASK:
    ;; Refactor the previous solution by moving the `<li>` into a separate
    ;; component called `ValuePropositionPoint` and `map` over the provided
    ;; `value-proposition-points` to produce the whole list.
    ;; TIP: When you paste the previous code and Calva messes up the indentation,
    ;;      select all of the wrongly indented code and press `tab` to fix it
    ;;      (or execute Format Selection)
    ;;
    ;; LEARNING OBJECTIVES:
    ;; - Creating and using Fulcro components
    ;; - Creating elements based on data
    ;;
    ;; RESOURCES:
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_the_anatomy_of_a_fulcro_component_query_ident_body
    (def value-proposition-points
      [{:proposition/label "Malleable"} {:proposition/label "Full-stack"} {:proposition/label "Well-designed"}])

    (defsc Root2 [_ _]
      {}
      "TODO")

    (config-and-render! Root2)
    ; (hint 2)

    ;; Task 2.b: Make sure you do not get the React error <<Each child in a list
    ;; should have a unique "key" prop.>> in the Chrome Console.
    ; (hint 2)
    ,))

(comment ; 3 "Externalizing data"
  (do
    ;; TASK:
    ;; We still want to render the same HTML but this time we want to read the
    ;; data from Fulcro's client DB, where it is inserted by `config-and-render!`.
    ;; Start from the previous solution but replace the hardcoded text and global
    ;; def with props.
    ;;
    ;; LEARNING OBJECTIVES:
    ;; - Use Fulcro state management as it is meant to be used, reading from the Client DB
    ;;
    ;; RESOURCES:
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_the_anatomy_of_a_fulcro_component_query_ident_body
    (defsc Root3 [_ _]
      {}
      "TODO")

    (config-and-render!
      Root3
      {:initial-db
       ;; NOTE: Normally the initial-db we pass here should be already normalized but
       ;; since we do not care about normalization and are happy with denormalized data
       ;; in this exercise, it is OK to pass in the data tree as-is.
       ;; BEWARE: The initial data is only processed at app initialization, i.e.
       ;; not after hot-reloading of a code change
       {:page/heading "<3> Fulcro is:"
        :page/value-proposition-points
                      [{:proposition/label "Malleable"}
                       {:proposition/label "Full-stack"}
                       {:proposition/label "Well-designed"}]}})

    ;(hint 3)
    ;; Tip: Use Fulcro Inspect to see the content of the client DB
    ;;      Also try to use the provided `(show-client-db)` function for that.
    ,))

(comment ; 4 "Insert data into the client DB with merge/merge!"
  (do
    ;; TASK:
    ;; Again we build on the previous solution but instead of inserting the data
    ;; at app initialization, we will revert to the render without :initial-db and insert them
    ;; manually with a call to `merge/merge!`
    ;;
    ;; LEARNING OBJECTIVES:
    ;; - Reason about data ingestion and representation
    ;; - Use Fulcro Inspect
    ;; - Understand merge! as a crucial step on the path to load!
    ;;
    ;; RESOURCES:
    ;; - the intro to merge-component! (https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_pre_study_merging_data_into_the_client_db_with_merge_component)
    ;;   is relevant because merge! behaves very similary to merge-component! - the difference is that it works for the Root instead of
    ;;   some child component and it takes a query instead of a component (because it doesn't need an ident)
    ;; - merge! docs: https://book.fulcrologic.com/#_using_com_fulcrologic_fulcro_componentsmerge
    (defsc Root4 [_ _]
      {}
      "TODO")

    (def app4 (config-and-render! Root4))

    ;; What do you think the client DB will look like? Think, write it down, then check it
    ;; using Fulcro Inspect - DB (or `(show-client-db)`)
    (merge/merge! app4 nil nil) ; TODO Implement
    ; (hint 4)
    ; (hint 4)
    ,))

(comment ; 5 "Normalization and merge! and merge-component!"
  (do
    ;; TASK:
    ;; Let's see how to insert data into the client DB for our UI to display!
    ;; We have this UI:
    ;; Root1
    ;;   Team(s)
    ;;     Player(s)
    ;;       Address
    ;; and a tree of data to give to it. There is now a few tasks awaiting you,
    ;; described in comments inside the code below. So read on!
    ;;
    ;; BEWARE: The provided code on purpose omits some things you would normally have included in your components
    ;;
    ;; LEARNING OBJECTIVES:
    ;; - Understand idents and their use by Fulcro
    ;; - Reason about data normalization
    ;; - Understand merge-component! as another crucial step on the path to load!
    ;;
    ;; RESOURCES:
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_components_query
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_components_ident
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_pre_study_merging_data_into_the_client_db_with_merge_component
    ;;   + https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_targeting_adding_references_to_the_new_data_to_existing_entities
    ;; - https://book.fulcrologic.com/#_using_mergemerge_component
    (defsc Address [_ {city :address/city}]
      {:query [:address/city]}
      (p "City: " city))

    (defsc Player [_ {:player/keys [name address]}]
      {:query [:player/id :player/name :player/address]}
      (li "name: " name " lives at: " ((comp/factory Address) address)))

    (def ui-player (comp/factory Player {:keyfn :player/id}))

    (defsc Team [_ {:team/keys [name players]}]
      {:query [:team/id :team/name :team/players]}
      (div (h2 "Team " name ":")
           (ol (map ui-player players))))

    (def ui-team (comp/factory Team {:keyfn :team/id}))

    (defsc Root5 [_ {teams :teams}]
      {:query [:teams]} ; NOTE: This is on purpose incomplete
      (div
        (h1 "Teams")
        (p "Debug: teams = " (dom/code (pr-str teams)))
        (map ui-team teams)))

    (def data-tree
      "The data that our UI should display"
      {:teams [#:team{:name "Hikers" :id :hikers
                      :players [#:player{:name "Jon" :address #:address{:city "Oslo"} :id 1}
                                #:player{:name "Ola" :address #:address{:city "Trondheim"} :id 2}]}]})

    ;; Render the app (without any data so far):
    (def app5 (config-and-render! Root5))

    ;; Now:
    ;; 1. Uncomment, complete, and run the merge/merge! call below to insert the data-tree into
    ;;    the client DB. Check the UI shows it.
    (comment (merge/merge! ...))
    ; (hint 5)
    ; (hint 5)

    ;; More tasks!
    ;; 2. Try to make the data normalized in the DB by adding idents. What do you
    ;;    expect the DB to look like? How does it actually look?
    ;;    (Don't worry if you fail - we will get back to it soon.)
    ; (hint 5)
    ; (hint 5)
    ; (hint 5)
    ;;
    ;; 3. Now, even if your idents are correct, the data still does not
    ;;    seem to be normalized as you would expect. Why? What did we forget?
    ;;    Is something wrong with our queries?! (Hint: yes, it is)
    ;;    Fix it and try again!
    ;;    Tip: force-reload the page to clean the client DB
    ; (hint 5)

    ;; 4. Rewrite the code to use merge/merge-component! instead.
    ;;    What is the correct target component? How to make sure the data looks
    ;;    just like before?
    ;;   Really try your best to make the right merge-component! call!
    ; (hint 5)
    ; (hint 5)
    ; (hint 5)
    ,))

(comment ; 6 Client-side mutations
  (do
    ;; TASK:
    ;; Enable the user to select individual elements or all at once
    ;; and delete these from the Client DB. (No remote in this exercises.)
    ;;
    ;; Your task is thus to implement the mutations `set-players-checked` and `delete-selected`,
    ;; and trigger them in the :onClick handlers below instead of the current `println`.
    ;; Keep to the suggested inputs of those mutations, to make comparison easier.
    ;;
    ;; (Note: We could have structured the mutations in a simpler way. But this one
    ;; provides you a sufficient challenge.)
    ;;
    ;; Tips:
    ;; - It is often useful to put most mutation logic into a pure helper fn state-map -> state-map
    ;;   (often with the same name but ending with *)
    ;; - Get the current state-map via `(app/current-state app6)` to play with your code in the REPL
    ;; - Use Fulcro Dev Tools to look at the database and Transactions
    ;; - Check `(hint 6)` if you need help
    ;;
    ;; LEARNING OBJECTIVES:
    ;; - React to user actions
    ;; - Create and trigger a mutation
    ;; - Use and manipulate the data in the Client DB
    ;; - Understand the need for UI-only (`:ui/*`) props
    ;; - Use Fulcro Inspect to explore the client DB and to troubleshoot transactions/mutations
    ;;
    ;; RESOURCES:
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_changing_global_data_and_performing_remote_calls_mutations
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#def-clientdb
    ;; - Fulcro Inspect and its parts: https://blog.jakubholy.net/2020/troubleshooting-fulcro/#_know_your_tools

    (defn make-player->team
      "A helper function to create a 'lookup' so that we can easily find a player's team.
      You might - or not :-) - find it useful in your mutations.

      Take a seq of teams and look into their `:team/players` to construct the map
      `player-id` -> `team-id`, useful to look up a player's team."
      ;; To try it out:
      ;; `(->> (app/current-state app6) :team/id vals make-player->team)`
      [teams]
      (into {}
            (for [{team-id :team/id
                   players :team/players} teams
                  [_ player-id]           players]
              [player-id team-id])))

    (defsc Player [this {:keys [player/id player/name ui/checked?]}]
      {:query [:player/id :player/name :ui/checked?]
       :ident :player/id}
      (li
        (input {:type    "checkbox"
                :checked (boolean checked?)
                :onClick #(println "TODO: trigger the mutation `(set-players-checked {:players [id] :value (not checked?)})`")})
        name))

    (def ui-player (comp/factory Player {:keyfn :player/id}))

    (defsc Team [this {:team/keys [name players] checked? :ui/checked?}]
      {:query [:team/id :team/name :ui/checked? {:team/players (comp/get-query Player)}]
       :ident :team/id}
      (let [all-checked? (and (seq players) (every? :ui/checked? players))]
        (div (h2 "Team " name ":")
             (label (input {:type    "checkbox"
                            :checked all-checked?
                            :onClick #(println "TODO: trigger the mutation `(set-players-checked {:players (map :player/id players) :value (not all-checked?)})`")})
                    "Select all")
             (ol (map ui-player players)))))

    (def ui-team (comp/factory Team {:keyfn :team/id}))

    (defsc Root6 [this {teams :teams}]
      {:query [{:teams (comp/get-query Team)}]}
      (form
        (h1 "Teams")
        (button {:type "button"
                 :onClick #(println "TODO: trigger the mutation `(delete-selected nil)`")}  ; TODO implement
                "Delete selected")
        (map ui-team teams)))

    (def app6 (config-and-render! Root6))

    (run!
      #(merge/merge-component! app6 Team % :append [:teams])
      [#:team{:name "Explorers" :id :explorers
              :players [#:player{:id 1 :name "Jo"}
                        #:player{:id 2 :name "Ola"}
                        #:player{:id 3 :name "Anne"}]}
       #:team{:name "Bikers" :id :bikers
              :players [#:player{:id 4 :name "Cyclotron"}]}])

    ,))

(comment ; 7 load!-ing data from a remote
  (do
    ;; TASK:
    ;; Learn how to load! data and practice using Fulcro Inspect
    ;; This is similar to #5 but with merge-component! replaced with load!
    ;; We now run a mock, in-browser server (with a real Pathom).
    ;; Read on to find the task you should do.
    ;;
    ;; LEARNING OBJECTIVES:
    ;; - Use load!, with targeting
    ;; - Create Pathom resolvers
    ;; - Use the EQL and Network tabs of Fulcro Inspect
    ;; - Use load markers to track the state of data loading
    ;;
    ;; RESOURCES:
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_loading_remote_data
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_targeting_adding_references_to_the_new_data_to_existing_entities
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_how_to
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_when_to_load
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/#_bonus_tracking_loading_state_with_load_markers

    ;; --- "Frontend" UI ---
    (defsc Address [_ {city :address/city}]
      {:query [:address/city]
       :ident :address/city}
      (p "City: " city))

    (defsc Player [_ {:player/keys [name address]}]
      {:query [:player/id :player/name {:player/address (comp/get-query Address)}]
       :ident :player/id}
      (li "Player: " name " lives at: " ((comp/factory Address) address)))

    (def ui-player (comp/factory Player {:keyfn :player/id}))

    (defsc Team [_ {:team/keys [name players]}]
      {:query [:team/id :team/name {:team/players (comp/get-query Player)}]
       :ident :team/id}
      (div (h2 "Team " name ":")
           (ol (map ui-player players))))

    (def ui-team (comp/factory Team {:keyfn :team/id}))

    (defsc Root7 [this {teams :teams :as props}]
      {:query [{:teams (comp/get-query Team)}]}
      (div
        ;; Code for task 2 (described further down) - un-comment and complete this code:
        ;(button {:type "button"
        ;         :onClick #(println "df/load! the data from here")} "Load data")
        (let [loading? false] ; scaffolding for TASK 5
          (cond
            loading? (p "Loading...")
            ;; ...
            :else
            (comp/fragment (h1 "Teams")
                           (map ui-team teams))))))

    ;; --- "Backend" resolvers to feed data to load! ---
    (defresolver my-very-awesome-teams [_ _] ; a global resolver
      {::pc/input  #{}
       ::pc/output [{:teams [:team/id :team/name
                             {:team/players [:player/id :player/name
                                             ;; NOTE: We need this 👇 instead of just `:player/address` so that autocomplete
                                             ;; in Fulcro Inspect - EQL understands this is address and can get to id, city
                                             {:player/address [:address/id]}]}]}]}
      {:teams [#:team{:name "Hikers" :id :hikers
                      :players [#:player{:id 1 :name "Luna" :address {:address/id 1}}
                                #:player{:id 2 :name "Sol" :address {:address/id 2}}]}]})

    (defresolver address [_ {id :address/id}] ; an ident resolver
      {::pc/input #{:address/id}
       ::pc/output [:address/id :address/city]}
      (case id
        1 #:address{:id 1 :city "Oslo"}
        2 #:address{:id 2 :city "Trondheim"}))

    ;; Render the app, with a backend using these resolvers
    (def app7 (config-and-render! Root7 {:resolvers [address my-very-awesome-teams]}))

    ;; TODO: TASK 1 - use `df/load!` to load data from the my-very-awesome-teams
    (println "TODO: df/load! should be invoked here...")
    ;; (Remember `(hint 7)` when in need.)
    ;; Now check Fulcro Inspect - the Transactions and Network tabs and explore the load there.
    ;; In both, click on the corresponding line to display details below. In the load's details
    ;; in the Network tab, press the [Send to query] button to show it in the EQL tab.
    ;; Run it from the EQL tab. Modify, run again.
    ;; - EQL tab - do [(Re)load Pathom Index] to get auto-completion for the queries and try to type some
    ;; - Index Explorer tab - do [Load index], explore the index (you might need to scroll up on the right side to see the selected thing)

    ;; TODO: TASK 2 - replace loading data during initialization (above) with loading them on-demand, on the button click

    ;; TODO: TASK 3 - split ident resolvers for a team and a player out of `my-very-awesome-teams`, as we did for address;
    ;;       Then play with them using Fulcro Inspect's EQL tab - fetch just the name of a particular person; ask for
    ;;       a property that does not exist (and check both the EQL tab and the Inspect's Network tab) - what does it look like?

    ;; TODO: TASK 4 - use targeting to fix a mismatch between a resolver and the UI: in `Root7`, rename `:teams` to `:all-teams`; how
    ;;       do you need to change the load! for this to work as before?
    ;;       Check in the Client DB that the changed data look as expected.

    ;; TODO: TASK 5 - Use Fulcro load markers to display "Loading..." instead of the content while loading the data (see Root7)

    ,))

(comment ; 8 Fix the graph
  (do
    ;; TASK:
    ;; Fix the code to actually show the list of cities.
    ;;
    ;; LEARNING OBJECTIVES:
    ;; - Understand the importance of connections in the frontend data and how
    ;;   to establish them
    ;; - Understand that you can add any arbitrary connection between components;
    ;;   they do not need to come only from the backend data
    ;;
    ;; RESOURCES:
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/index.html#_components_initial_state
    ;; - https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/index.html#_components_query
    ;;
    ;; TIP: You need to fix 4 places, most of them in Root8
    ;;
    (defsc Menu [this {:keys [cities selected-city]}]
      {:ident (fn [] [:component/id ::Menu])
       :query [:cities :selected-city]}
      ;; Note: This is not a very good way of using a select :-)
      (dom/select {:value (or selected-city "Select one:")
                   :onChange #(do (println "Selected city:" (.-value (.-target %)))
                                (m/set-string! this :selected-city :event %))}
        (->> (cons "Select one:" cities)
             (mapv #(dom/option {:key %, :value %} %)))))

    (def ui-menu (comp/factory Menu))

    (defsc Root8 [_ props]
      {:query []}
      (dom/div
        (h1 "Select a city!")
        (ui-menu {:TODO "fix this and other places"})))

    (defresolver cities [_ _]
      {::pc/input #{}
       ::pc/output [:cities]}
      {:cities ["Linköping" "Oslo" "Prague"]})

    ;; Render the app, with a backend using these resolvers
    (def app8 (config-and-render! Root8 {:resolvers [cities]}))

    ;; We want to load :cities into the Menu component because it is the only
    ;; one needing the data (rather then polluting root with it and having to
    ;; use a Link Query)
    (df/load! app8 :cities nil {:target (conj (comp/get-ident Menu {}) :cities)})
    ))

;; TODO Additional exercises:
;; - computed props for passing a callback from the parent
;; - create/delete/create+delete <> tmpids; simulated failure => undo the optimistic change?
;; - link queries & more from https://blog.jakubholy.net/2020/fulcro-divergent-ui-data/
;; - anything else from the Minim. Fulcro Tutorial should be added?
;;   - Initial state propagation?
;;   - Computed props for a callback or parent-visible prop
;;   - pre-merge ?! / loading dyn. data for a defsc containing a router x we did not learn routers
;;   - Link Query? But not covered by MFT
;; Other:
;; @peterdee:  incremental loading of big trees of data. Incremental loading is probably discussed
;; adequately in the Developer Guide, but not with recursive queries, and incremental expansion of a UI tree,
;; I think. If that seems like too much an edge case, maybe something simpler with trees.


text:
(ns holyjak.solutions
  "Solutions to the exercises - check them against your and peek at them if you get stuck."
  (:require
    [holyjak.fulcro-exercises.impl :refer [hint config-and-render! show-client-db]]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]
    [com.fulcrologic.fulcro.algorithms.data-targeting :as targeting]
    [com.fulcrologic.fulcro.algorithms.normalized-state :as norm]
    [com.fulcrologic.fulcro.application :as app]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc transact!]]
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.fulcrologic.fulcro.mutations :refer [defmutation]]
    [com.fulcrologic.fulcro.dom :as dom :refer [button div form h1 h2 h3 input label li ol p ul]]
    [com.wsscode.pathom.connect :as pc :refer [defresolver]]
    [com.fulcrologic.fulcro.mutations :as m]))

;; TIP: Use your editor's powers to collapse all the comments and only open the one you want to study
;;      (Cursive: 'Collapse All', `Shift Cmd -` on Mac)

(comment ; 1 "Hard-coded DOM"
  (do
    (defsc Root1 [_ _]
           {}
           #_"TODO"
           (div
             (h1 :#title {:style {:textAlign "center"}} "Fulcro is:")
             (ul
               (li "Malleable")
               (li "Full-stack")
               (li "Well-designed"))))

    (config-and-render! Root1)))

(comment ; 2 "Extracting a child component"
  (do
    (def value-proposition-points
      [{:proposition/label "Malleable"} {:proposition/label "Full-stack"} {:proposition/label "Well-designed"}])

    (defsc ValuePropositionPoint [_ {:proposition/keys [label]}]
           (li label))

    (def ui-value-proposition-point (comp/factory ValuePropositionPoint {:keyfn :proposition/label}))

    (defsc Root2 [_ _]
           {}
           #_"TODO"
           (div
             (h1 :#title {:style {:textAlign "center"}} "<2> Fulcro is:")
             (ul (map ui-value-proposition-point value-proposition-points))))

    (config-and-render! Root2)
    ,))

(comment ; 3 "Externalizing data"
  (do
    (defsc ValuePropositionPoint [_ {:proposition/keys [label]}]
           {:query [:proposition/label]}
           (li label))

    (def ui-value-proposition-point (comp/factory ValuePropositionPoint {:keyfn :proposition/label}))

    (defsc Root3 [_ {:page/keys [heading value-proposition-points]}]
           {:query [:page/heading {:page/value-proposition-points (comp/get-query ValuePropositionPoint)}]}
           #_"TODO"
           (div
             (h1 :#title {:style {:textAlign "center"}} "hdr:" heading)
             (ul (map ui-value-proposition-point value-proposition-points))))

    (config-and-render!
      Root3
      {:initial-db
       {:page/heading "<3> Fulcro is:"
        :page/value-proposition-points
                      [{:proposition/label "Malleable"}
                       {:proposition/label "Full-stack"}
                       {:proposition/label "Well-designed"}]}})
    ,))

(comment ; 4 "Insert data into the client DB with merge/merge!"
  (do
    (defsc ValuePropositionPoint [_ {:proposition/keys [label]}]
           {:query [:proposition/label]}
           (li label))

    (def ui-value-proposition-point (comp/factory ValuePropositionPoint {:keyfn :proposition/label}))

    (defsc Root4 [_ {:page/keys [heading value-proposition-points]}]
           {:query [:page/heading {:page/value-proposition-points (comp/get-query ValuePropositionPoint)}]}
           #_"TODO"
           (div
             (h1 :#title {:style {:textAlign "center"}} "hdr:" heading)
             (ul (map ui-value-proposition-point value-proposition-points))))

    (def app4 (config-and-render! Root4))

    (merge/merge!
      app4
      {:page/heading "<4> Fulcro is:"
       :page/value-proposition-points
                     [{:proposition/label "Malleable"}
                      {:proposition/label "Full-stack"}
                      {:proposition/label "Well-designed"}]}
      (comp/get-query Root4))
    ,))

(comment ; 5 "Normalization and merge-component!"
  ;; VARIANT 5.1
  (do
    (defsc Address [_ {city :address/city}]
           {:query [:address/city]}
           (p "City: " city))

    (defsc Player [_ {:player/keys [name address]}]
           {:query [:player/id :player/name :player/address]}
           (li "name: " name " lives at: " ((comp/factory Address) address)))

    (def ui-player (comp/factory Player {:keyfn :player/id}))

    (defsc Team [_ {:team/keys [name players]}]
           {:query [:team/id :team/name :team/players]}
           (div (h2 "Team " name ":")
                (ol (map ui-player players))))

    (def ui-team (comp/factory Team {:keyfn :team/id}))

    (defsc Root5 [_ {teams :teams}]
           {:query [:teams]} ; NOTE: This is on purpose incomplete
           (div
             (h1 "Teams")
             (p "Debug: teams = " (dom/code (pr-str teams)))
             (map ui-team teams)))

    (def data-tree
      "The data that our UI should display"
      {:teams [#:team{:name "Hikers" :id :hikers
                      :players [#:player{:name "Jon" :address #:address{:city "Oslo"} :id 1}
                                #:player{:name "Ola" :address #:address{:city "Trondheim"} :id 2}]}]})

    ;; Render the app (without any data so far):
    (def app5 (config-and-render! Root5))

    (merge/merge! app5 data-tree (comp/get-query Root5))
    ,))

(comment ; 5 "Normalization and merge-component!"
  ;; VARIANT 5.2 + 5.3
  (do
    (defsc Address [_ {city :address/city}]
           {:query [:address/city]
            :ident :address/city}
           (p "City: " city))

    (defsc Player [_ {:player/keys [name address]}]
           {:query [:player/id :player/name {:player/address (comp/get-query Address)}]
            :ident :player/id}
           (li "name: " name " lives at: " ((comp/factory Address) address)))

    (def ui-player (comp/factory Player {:keyfn :player/id}))

    (defsc Team [_ {:team/keys [name players]}]
           {:query [:team/id :team/name {:team/players (comp/get-query Player)}]
            :ident :team/id}
           (div (h2 "Team " name ":")
                (ol (map ui-player players))))

    (def ui-team (comp/factory Team {:keyfn :team/id}))

    (defsc Root5 [_ {teams :teams}]
           {:query [{:teams (comp/get-query Team)}]} ; NOTE: This is on purpose incomplete
           (div
             (h1 "Teams")
             (p "Debug: teams = " (dom/code (pr-str teams)))
             (map ui-team teams)))

    (def data-tree
      "The data that our UI should display"
      {:teams [#:team{:name "Hikers" :id :hikers
                      :players [#:player{:name "Jon" :address #:address{:city "Oslo"} :id 1}
                                #:player{:name "Ola" :address #:address{:city "Trondheim"} :id 2}]}]})

    ;; Render the app (without any data so far):
    (def app5 (config-and-render! Root5))

    (merge/merge! app5 data-tree (comp/get-query Root5))
    ,))

(comment ; 5 "Normalization and merge-component!"
  ;; VARIANT 5.4

  ;; !!! BEWARE: Only the merge-component! part provided here,
  ;;     which would replace the original call to merge/merge!;
  ;;     the rest is the same.
  ;; <THE SAME CODE AS IN 5.3 HERE...>
  (merge/merge-component!
    app5
    Team
    (-> data-tree :teams first)
    :append [:teams])
  ,)

(comment ; 6 Client-side mutations
  ;; NOTE: There are many ways to implement the details, this is just one
  (do

    (defn set-player-checked* [new-value state-map id]
      (assoc-in state-map [:player/id id :ui/checked?] new-value))

    (defmutation set-players-checked [{:keys [players value]}]
      (action [{:keys [state]}]
        (swap! state #(reduce (partial set-player-checked* value) % players))))

    (defn make-player->team
      "Take a seq of teams and look into their `:team/players` to construct the map
      `player-id` -> `team-id`, useful to look up a player's team."
      ;; To try it out:
      ;; `(->> (app/current-state app6) :team/id vals make-player->team)`
      [teams]
      (into {}
            (for [{team-id :team/id
                   players :team/players} teams
                  [_ player-id]           players]
              [player-id team-id])))

    ;; OPTION B: DIY deletion:
    ;(defn delete-player [state-map [player-id team-id]]
    ;  (-> state-map
    ;      (update :player/id dissoc player-id)
    ;      (merge/remove-ident* [:player/id player-id]
    ;                           [:team/id team-id :team/players])))

    (defn delete-selected* [{player-map :player/id :as state-map}]
      (let [player->team  (make-player->team (-> state-map :team/id vals))
            selected-player-ids
                          (->> player-map
                               vals
                               (filter :ui/checked?)
                               (map :player/id))
            player-teams (map player->team selected-player-ids)]
        ;; OPTION B: DIY deletion:
        #_(reduce
            delete-player
            state-map
            (map vector selected-player-ids player-teams))
        ;; OPTION A: using built-in helpers
        (reduce
          norm/remove-entity
          state-map
          (map #(vector :player/id %) selected-player-ids))))

    (defmutation delete-selected [_]
      (action [{:keys [state]}]
        (swap! state delete-selected*)))

    (defsc Player [this {:keys [player/id player/name ui/checked?]}]
           {:query [:player/id :player/name :ui/checked?]
            :ident :player/id}
           (li
             (input {:type    "checkbox"
                     :checked (boolean checked?)
                     :onClick #(transact! this [(set-players-checked {:players [id] :value (not checked?)})])})
             name))

    (def ui-player (comp/factory Player {:keyfn :player/id}))

    (defsc Team [this {:team/keys [name players] checked? :ui/checked?}]
           {:query [:team/id :team/name :ui/checked? {:team/players (comp/get-query Player)}]
            :ident :team/id}
           (let [all-checked? (boolean (and (seq players) (every? :ui/checked? players)))]
             (div (h2 "Team " name ":")
                  (label (input {:type    "checkbox"
                                 :checked all-checked?
                                 :onClick #(transact! this [(set-players-checked {:players (map :player/id players)
                                                                                :value   (not all-checked?)})])})
                         "Select all")
                  (ol (map ui-player players)))))

    (def ui-team (comp/factory Team {:keyfn :team/id}))

    (defsc Root6 [this {teams :teams}]
           {:query [{:teams (comp/get-query Team)}]}
           (form
             (h1 "Teams")
             (button {:type "button"
                      :onClick #(transact! this [(delete-selected nil)])} "Delete selected")
             (map ui-team teams)))

    (def app6 (config-and-render! Root6))

    (run!
      #(merge/merge-component! app6 Team % :append [:teams])
      [#:team{:name "Explorers" :id :explorers
              :players [#:player{:id 1 :name "Jo"}
                        #:player{:id 2 :name "Ola"}
                        #:player{:id 3 :name "Anne"}]}
       #:team{:name "Bikers" :id :bikers
              :players [#:player{:id 4 :name "Cyclotron"}]}])

    ,))

(comment ; 7 load!-ing data from a remote
  (do
    ;; VARIANT 7.1 and 7.2

    ;; --- "Frontend" UI ---
    (defsc Address [_ {city :address/city}]
      {:query [:address/city]
       :ident :address/city}
      (p "City: " city))

    (defsc Player [_ {:player/keys [name address]}]
      {:query [:player/id :player/name {:player/address (comp/get-query Address)}]
       :ident :player/id}
      (li "Player: " name " lives at: " ((comp/factory Address) address)))

    (def ui-player (comp/factory Player {:keyfn :player/id}))

    (defsc Team [_ {:team/keys [name players]}]
      {:query [:team/id :team/name {:team/players (comp/get-query Player)}]
       :ident :team/id}
      (div (h2 "Team " name ":")
           (ol (map ui-player players))))

    (def ui-team (comp/factory Team {:keyfn :team/id}))

    (defsc Root7 [this {teams :teams}]
      {:query [{:teams (comp/get-query Team)}]}
      (div
        (button {:type "button" ; VARIANT 7.2; comment out for 7.1
                 :onClick #(df/load! this :teams Team)} "Load data")
        (let [loading? false] ; scaffolding for TASK 5
          (cond
            loading? (p "Loading...")
            ;; ...
            :else
            (comp/fragment (h1 "Teams")
                           (map ui-team teams))))))

    ;; --- "Backend" resolvers to feed data to load! ---
    (defresolver my-very-awesome-teams [_ _] ; a global resolver
                 {::pc/input  #{}
                  ::pc/output [{:teams [:team/id :team/name
                                        {:team/players [:player/id :player/name {:player/address [:address/id]}]}]}]}
                 {:teams [#:team{:name "Hikers" :id :hikers
                                 :players [#:player{:id 1 :name "Luna" :address {:address/id 1}}
                                           #:player{:id 2 :name "Sol" :address {:address/id 2}}]}]})

    (defresolver address [_ {id :address/id}] ; an ident resolver
                 {::pc/input #{:address/id}
                  ::pc/output [:address/id :address/city]}
                 (case id
                   1 #:address{:id 1 :city "Oslo"}
                   2 #:address{:id 2 :city "Trondheim"}))

    ;; Render the app, with a backend using these resolvers
    (def app7 (config-and-render! Root7 {:resolvers [address my-very-awesome-teams]}))

    (df/load! app7 :teams Team) ; VARIANT 7.1; comment out for 7.2

    ;; TODO: TASK 3 - split ident resolvers for a team and a player out of my-very-awesome-teams, as we did for address;
    ;;       Then play with them using Fulcro Inspect's EQL tab - fetch a particular person with just the name; ask for
    ;;       a property that does not exist (and check both the EQL tab and the Inspect's Network tab)

    ,))

(comment ; VARIANT 7.3 - separate resolvers
  (do

    ;; BEWARE: Only the resolvers and render presented here, the UI is the same as above.
    ;; <THE SAME FRONTEND CODE AS IN 7.1 + 7.2 HERE...>

    ;; --- "Backend" resolvers to feed data to load! ---
    (defresolver my-very-awesome-teams [_ _] ; a global resolver
      {::pc/input  #{}
       ::pc/output [{:teams [:team/id :team/name {:team/players [:player/id]}]}]}
      {:teams [#:team{:name "Hikers" :id :hikers ; we could also return just [{:team/id :hikers}] and leverage the team resolver below
                      :players [#:player{:id 1}
                                #:player{:id 2}]}]})

    (defresolver team [_ {id :team/id}] ; an ident resolver
      {::pc/input #{:team/id}
       ::pc/output [:team/id :team/name {:team/players [:player/id]}]}
      (case id
        :hikers #:team{:id :hikers :name "Hikers"
                       :players [#:player{:id 1} #:player{:id 2}]}))

    (defresolver player [_ {id :player/id}] ; an ident resolver
      {::pc/input #{:player/id}
       ::pc/output [:player/id :player/name {:player/address [:address/id]}]}
      (case id
        1 #:player{:id 1 :name "Luna" :address {:address/id 1}}
        2 #:player{:id 2 :name "Sol" :address  {:address/id 2}}))

    (defresolver address [_ {id :address/id}] ; an ident resolver
      {::pc/input #{:address/id}
       ::pc/output [:address/id :address/city]}
      (case id
        1 #:address{:id 1 :city "Oslo"}
        2 #:address{:id 2 :city "Trondheim"}))

    ;; Render the app, with a backend using these resolvers
    (def app7 (config-and-render! Root7 {:resolvers [address my-very-awesome-teams player team]}))

    ,))

(comment ; VARIANT 7.4 - targeting
  (do
    ;; BEWARE: Only the changed parts presented here, the rest is the same as above.
    ;; <THE SAME CODE AS ABOVE EXCLUDING Root7 HERE...>
    (defsc Root7 [this {teams :all-teams}]
      {:query [{:all-teams (comp/get-query Team)}]}
      (div
        (button {:type "button"
                 :onClick #(df/load! this :teams Team {:target (targeting/replace-at [:all-teams])})} "Load data")
        (let [loading? false] ; scaffolding for TASK 5
          (cond
            loading? (p "Loading...")
            ;; ...
            :else
            (comp/fragment (h1 "Teams")
                           (map ui-team teams))))))

    ,))

(comment ; VARIANT 7.5 - load marker
  (do
    ;; BEWARE: Only the changed parts presented here, the rest is the same as above.
    ;; <THE SAME CODE AS ABOVE EXCLUDING Root7 HERE...>
    (defsc Root7 [this {teams :all-teams :as props}]
      {:query [{:all-teams (comp/get-query Team)} [df/marker-table :teams]]}
      (div
        (button {:type "button"
                 :onClick #(df/load! this :teams Team {:target (targeting/replace-at [:all-teams])
                                                       :marker :teams})}
                "Load data")
        (let [marker (get props [df/marker-table :teams])]
          (cond
            (df/loading? marker) (p "Loading...")
            ;; ...
            :else
            (comp/fragment (h1 "Teams")
                           (map ui-team teams))))))
    ,))

(comment ; 8 Fix the graph
  (do
    (defsc Menu [this {:keys [cities selected-city]}]
      {:ident (fn [] [:component/id ::Menu])
       :query [:cities :selected-city]
       :initial-state {}}
      ;; Note: This is not a very good way of using a select :-)
      (dom/select {:value (or selected-city "Select one:")
                   :onChange #(do (println "Selected city:" (.-value (.-target %)))
                                (m/set-string! this :selected-city :event %))}
        (->> (cons "Select one:" cities)
             (mapv #(dom/option {:key %, :value %} %)))))

    (def ui-menu (comp/factory Menu))

    (defsc Root8 [_ props]
      ;; The key discovery is that we can add _arbitrary_ "edges" to the client DB / graph
      ;; (here Root -(:menu)-> Menu) f.ex. by setting them up via :initial-state
      {:query [{:menu (comp/get-query Menu)}]
       :initial-state {:menu {}}}
      (dom/div
        (h1 "Select a city!")
        (ui-menu (:menu props))))

    (defresolver cities [_ _]
      {::pc/input #{}
       ::pc/output [:cities]}
      {:cities ["Linköping" "Oslo" "Prague"]})

    (def app8 (config-and-render! Root8 {:resolvers [cities]}))

    (df/load! app8 :cities nil {:target (conj (comp/get-ident Menu {}) :cities)})
    ))


text:
(ns holyjak.fulcro-exercises.mock-server
  "Simulate a Pathom backend but running in the browser

  Source: https://github.com/fulcrologic/fulcro-developer-guide/blob/master/src/book/book/pathom.cljs"
  (:require
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.networking.mock-server-remote :refer [mock-http-server]]
    [com.wsscode.pathom.core :as p]
    [com.wsscode.pathom.connect :as pc]))

(pc/defresolver index-explorer [env _]
  {::pc/input  #{:com.wsscode.pathom.viz.index-explorer/id}
   ::pc/output [:com.wsscode.pathom.viz.index-explorer/index]}
  {:com.wsscode.pathom.viz.index-explorer/index
   (-> (get env ::pc/indexes)
       (update ::pc/index-resolvers #(into {} (map (fn [[k v]] [k (dissoc v ::pc/resolve)])) %))
       (update ::pc/index-mutations #(into {} (map (fn [[k v]] [k (dissoc v ::pc/mutate)])) %)))})

(defn new-parser [my-resolvers]
  (p/parallel-parser
    {::p/env     {::p/reader [p/map-reader
                              pc/parallel-reader
                              pc/open-ident-reader]}
     ::p/mutate  pc/mutate-async
     ::p/plugins [(pc/connect-plugin {::pc/register (conj my-resolvers index-explorer)})
                  p/error-handler-plugin
                  p/request-cache-plugin
                  (p/post-process-parser-plugin p/elide-not-found)]}))

(defn mock-remote
  ([resolvers env]
   (let [parser    (new-parser resolvers)
         transmit! (:transmit! (mock-http-server {:parser (fn [req] (parser env req))}))]
     {:remote {:transmit! (fn [this send-node]
                            (js/setTimeout
                              #(transmit! this send-node)
                              500))}}))
  ([resolvers]
   (mock-remote resolvers {})))


text:
(ns holyjak.fulcro-exercises.impl
  (:require
    [holyjak.fulcro-exercises.mock-server :refer [mock-remote]]
    [clojure.string :as str]
    [com.fulcrologic.fulcro.application :as app]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]))

(declare hints)
(defonce hints-shown (atom nil))

(defn hint [exercise-nr]
  {:pre [(int? exercise-nr)
         (not (neg? exercise-nr))]}
  (let [exercise-hints   (get hints exercise-nr)
        shown-previously (get @hints-shown exercise-nr 0)
        next?            (< shown-previously (count exercise-hints))
        more?            (not= (inc shown-previously) (count exercise-hints))
        printit!         #(println (str/join "\n\n" %))]
    (cond
      (nil? exercise-hints) "Sorry, no hints available for this exercise"

      next?
      (do (swap! hints-shown update exercise-nr (fnil inc 0))
          (cond-> (subvec exercise-hints shown-previously (inc shown-previously))
                  more? (conj "Repeat for more...")
                  true printit!))

      :else
      (printit! exercise-hints))))

(defonce current-app (atom nil))

(defn show-client-db
  "Print the current content of Fulcro's Client DB, if any."
  []
  (some-> @current-app
          ::app/state-atom
          deref
          cljs.pprint/pprint))

(defn config-and-render!
  "Renders the given root component, also creating a new Fulcro app for it.

  Args:
  - `RootComponent` - the class of the component to render
  - `opts` - options, including:
    - `:resolvers` - a sequence of Pathom resolvers; if provided, a Pathom 'backend'
       (though running in the browser) with these resolvers will be set up
    - `:initial-db` - the initial value of the client DB (normalized)

  Return the new Fulcro app."
  ([RootComponent] (config-and-render! RootComponent nil))
  ([RootComponent {:keys [initial-db resolvers]}]
   (let [current-root? (= (some-> @current-app app/root-class .-name)
                          (.-name RootComponent))
         remotes       (some-> resolvers seq mock-remote)
         app           (if current-root? @current-app (app/fulcro-app
                                                        {:initial-db initial-db
                                                         :remotes    remotes}))]

     (when remotes
       (println "LOG: Configured the remote" (-> remotes keys first) "for the Fulcro App"))
     (reset! current-app app)
     (println "LOG: Rendering" RootComponent "...")
     (app/mount! app RootComponent "app" #_{:initialize-state? (some? initial-db)}) ; FIXME:!!!!
     app)))

(defn refresh []
  (when-let [app @current-app]
    (app/mount! app
                (app/root-class app)
                "app")))

(comment
  (hint 4)
  (comp/component-name (app/root-class @current-app)),)

(def hints
  {0 ["Awesome, I see you got the hang of it!"
      "No more hints here, sorry!"]
   2 ["Create defsc ValuePropositionPoint, remember to use comp/factory. You do not need any query, just use the props the parent passes in."
      "2.b Look at the options that comp/factory takes. Remember you can use :proposition/label as a function."]
   3 ["Remember to make a join with comp/get-query to include the child's (i.e. ValuePropositionPoint's) query in Root"]
   4 ["Simply pass in to `merge!` the same data-tree that we passed as initial-db to `config-and-render!` in the previous exercise."
      "`merge!` also needs a query; and since we are passing all the app data, it should be the root query"]
   5 ["5.1a You want to use the query of the root component, Root5"
      "5.1b Use `comp/get-query` to get it!"
      "5.2a Normalization: Add idents to all components (but the root). Use the https://book.fulcrologic.com/#_keyword_idents form."
      "5.2b Normalization: Use :team/id, :player/id, :address/city as the idents (same as `(fn [] [:team/id (:team/id <props>)])` ...). But also remember to add the IDs to the queries!"
      "5.2c Normalization: Your data still is not normalized - and that is expected. We address that in the next exercise, 5.3"
      "5.3 Normalization fix: remember to use comp/get-query in your queries!"
      "5.4a merge-component!: We cannot use Root5 because it has no ident and thus we cannot merge the whole tree. But we can merge a team's data using the correct component."
      "5.4b Inserting the team's data is not enough - we also need to re-establish the 'edge' between it and `:teams`. Look at the client DB and see it is missing! Look at what options merge-component! takes to support this."
      "5.4c Add the arguments `:append [:teams]`"]
   6 ["You only need the `(action [{:keys [state]}] ...`) part of the mutation (where `state` is an atom containing the state-map a.k.a. client DB)"
      "Remember that you must not only remove the player her/himself but also any reference to her/him from a team's players list. `merge/remove-ident*` will help with that."]
   7 ["7.1a The `server-property` passed to load should be a property a global resolver returns. In this case `:teams`."
      "7.1b The component class passed to load! holds the query defining what to fetch for each element. Since we are getting a list of teams, the component should be `Team`"
      "7.2 load! does internally transact a mutation and can be called as-is, directly from the :onClick. Use the component's `this` instead of `app7` there."
      "7.3 After you factor out a resolver, remember to add it to the `:resolvers` list, as Pathom needs to be informed of it."
      "7.4 Use the `:target` option of load! with `(targeting/replace-at ..)`."]
   8 ["8a Fix the page's query first"
      "8b Remember the query must compose up to the parent"
      "8c Tip: You can make up an arbitrary keyword for the Menu's data in Root, e.g. `:menu` is fine"
      "8d Query is not enough - you need connection between Root and Menu in the client DB's data"
      "8e Namely you need `:menu [:component/id ::Menu]` at the top of the Client DB"
      "8f You can leverage initial-state to set that connection up"]})


text:
(ns holyjak.fulcro-exercises.puzzles.puzzles-ws
  "After you have completed the exercises, you can continue
  with these puzzles - pieces of code that you are expected to fix.

  See the README.md for more information."
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [nubank.workspaces.core :as ws]
    [nubank.workspaces.model :as wsm]
    [nubank.workspaces.card-types.fulcro3 :as ct.fulcro]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]))

;;----------------------------------------------------------------------------------
;; PUZZLE 1
(m/defmutation toggle-color [_]
  (action [{:keys [state]}]
          (swap! state update :ui/red? not)))

(defsc ColorChangingSquare [this {:ui/keys [red?]}]
   {:query [:ui/red?]
    :ident (fn [] [:component/id :ColorChangingSquare])
    :initial-state {:ui/red? false}}
   (dom/div {:style {:backgroundColor (if red? "red" "blue")
                     :padding "1em"
                     :color "white"}}
      (dom/p "The button bellow should change the background color from blue
             to red (and back) but it does not work. Fix it.")
      (dom/button {:onClick #(comp/transact! this [(toggle-color)])
                   :style {:backgroundColor "unset"
                           :color "white"}}
                (str "Make " (if red? "blue" "red")))))

(ws/defcard p1-change-background-button-puzzle
            {::wsm/card-width 2 ::wsm/card-height 6}
            (ct.fulcro/fulcro-card
              {::ct.fulcro/root       ColorChangingSquare
               ::ct.fulcro/wrap-root? true}))

;;----------------------------------------------------------------------------------
;; PUZZLE ?


text:
(ns holyjak.fulcro-exercises.puzzles.solutions-ws
  "Solutions to the puzzles - have a look to compare with
  your solution or when you get stuck."
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [nubank.workspaces.core :as ws]
    [nubank.workspaces.model :as wsm]
    [nubank.workspaces.card-types.fulcro3 :as ct.fulcro]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]))

;;----------------------------------------------------------------------------------
;; PUZZLE 1
(m/defmutation toggle-color [_]
  (action [{:keys [state]}]
          ;;; NOTE: There is also `(m/toggle! <component this> :ui/red?)` so ideally
          ;;        we would use that *inside* the component instead of the (transact! ...) call
          ;;        but that is not allowed by the task's specification :-)
          (swap! state update-in [:component/id :ColorChangingSquare :ui/red?] not)))

(defsc ColorChangingSquare [this {:ui/keys [red?]}]
  {:query [:ui/red?]
   :ident (fn [] [:component/id :ColorChangingSquare])
   :initial-state {:ui/red? false}}
  (dom/div {:style {:backgroundColor (if red? "red" "blue")
                    :padding "1em"
                    :color "white"}}
    (dom/p "The button bellow should change the background color from blue
             to red (and back) but it does not work. Fix it.")
    (dom/button {:onClick #(comp/transact! this [(toggle-color)])
                 :style {:backgroundColor "unset"
                         :color "white"}}
      (str "Make " (if red? "blue" "red")))))

(ws/defcard p1-change-background-button-solution
            {::wsm/card-width 2 ::wsm/card-height 6}
            (ct.fulcro/fulcro-card
              {::ct.fulcro/root       ColorChangingSquare
               ::ct.fulcro/wrap-root? true}))

;;----------------------------------------------------------------------------------
;; PUZZLE ?


text:
	  <title>fulcro-exercises</title>

Fulcro Exercises

Your Fulcro content will show up here after you config-and-render! the app


text:
<!DOCTYPE html>

    <title>F.Exerc. - Workspaces</title>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1" name="viewport">


text:
{:hooks {:analyze-call {com.fulcrologic.fulcro.mutations/defmutation com.fulcrologic.fulcro.clj-kondo-hooks/defmutation
                        com.fulcrologic.guardrails.core/>defn com.fulcrologic.fulcro.clj-kondo-hooks/>defn}}
 :linters {:clj-kondo.fulcro.defmutation/handler-arity {:level :error}
           :clj-kondo.fulcro.>defn/signature-mismatch {:level :error}}
 :lint-as {com.fulcrologic.fulcro.components/defsc                  clojure.core/defn
           com.fulcrologic.fulcro.routing.dynamic-routing/defrouter clojure.core/defn
           com.fulcrologic.fulcro.ui-state-machines/defstatemachine clojure.core/def
           com.fulcrologic.guardrails.core/>def                     clojure.core/def
           com.fulcrologic.guardrails.core/>defn                    clojure.core/defn
           com.fulcrologic.rad.attributes/defattr                   clojure.core/def
           com.fulcrologic.rad.report/defsc-report                  clojure.core/defn
           com.fulcrologic.rad.form/defsc-form                      clojure.core/defn
           com.fulcrologic.rad.authorization/defauthenticator       clojure.core/def}}


text:
(ns com.fulcrologic.fulcro.clj-kondo-hooks
  (:require [clj-kondo.hooks-api :as api]))

(defn defmutation
  [{:keys [node]}]
  (let [args          (rest (:children node))
        mutation-name (first args)
        ?docstring    (when (string? (api/sexpr (second args)))
                        (second args))
        args          (if ?docstring
                        (nnext args)
                        (next args))
        params        (first args)
        handlers      (rest args)
        handler-syms  (map (comp first :children) handlers)
        bogus-usage   (api/vector-node (vec handler-syms))
        letfn-node    (api/list-node
                       (list
                        (api/token-node 'letfn)
                        (api/vector-node (vec handlers))
                        bogus-usage))
        new-node      (api/list-node
                       (list
                        (api/token-node 'defn)
                        mutation-name
                        params
                        letfn-node))]
    (doseq [handler handlers]
      (let [hname (some-> handler :children first api/sexpr str)
            argv  (some-> handler :children second)]
        (when-not (= 1 (count (api/sexpr argv)))
          (api/reg-finding! (merge
                             (meta argv)
                             {:message (format "defmutation handler '%s' should be a fn of 1 arg" hname)
                              :type    :clj-kondo.fulcro.defmutation/handler-arity})))))
    {:node new-node}))

(defn >defn
  [{:keys [node]}]
  (let [args       (rest (:children node))
        fn-name    (first args)
        ?docstring (when (string? (api/sexpr (second args)))
                     (second args))
        args       (if ?docstring
                     (nnext args)
                     (next args))
        argv       (first args)
        gspec      (second args)
        body       (nnext args)
        new-node   (api/list-node
                    (list*
                     (api/token-node 'defn)
                     fn-name
                     argv
                     gspec
                     body))]
    (when (not= (count (api/sexpr argv))
                (count (take-while #(not= '=> %) (api/sexpr gspec))))
      (api/reg-finding! (merge (meta gspec)
                               {:message "Guardrail spec does not match function signature"
                                :type    :clj-kondo.fulcro.>defn/signature-mismatch})))
    {:node new-node}))


text:
{:lint-as {com.wsscode.pathom.connect/defmutation clojure.core/defn
           com.wsscode.pathom.connect/defresolver clojure.core/defn}}


text:
{:lint-as {com.wsscode.async.async-clj/deftest-async  clojure.test/deftest
           com.wsscode.async.async-clj/go-try-stream  clojure.core/let
           com.wsscode.async.async-clj/let-chan       clojure.core/let
           com.wsscode.async.async-clj/let-chan*      clojure.core/let
           com.wsscode.async.async-cljs/deftest-async clojure.test/deftest
           com.wsscode.async.async-cljs/go-try-stream clojure.core/let
           com.wsscode.async.async-cljs/let-chan      clojure.core/let
           com.wsscode.async.async-cljs/let-chan*     clojure.core/let}}


text:
{:lint-as
 {rewrite-clj.zip/subedit-> clojure.core/->
  rewrite-clj.zip/subedit->> clojure.core/->>
  rewrite-clj.zip/edit-> clojure.core/->
  rewrite-clj.zip/edit->> clojure.core/->>}}


text:
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org>

repo_name: gwygonik/SquiggleDraw

repo_language: Processing

mime_type: text/x-java


text:
# SquiggleDraw

SquiggleDraw will create a SVG file from an image, using the brightness to change the amplitude of sine waves. It is easier to look at the examples below to better understand. :-)

You can use the saved SVG file to print from Inkscape/AI/etc, draw with a pen on a pen plotter (like the awesome AxiDraw), or engrave with a lasercutter (it looks cool!)

This is code for Processing (http://processing.org)

Additional dependency: Download and install controlP5 in your processing libraries directory. ( http://www.sojamo.de/libraries/controlP5/ )

Features:
* Simple UI to load/save images, adjust parameters related to the sine waves, and basic image adjustements
* Saved SVG is larger than original image for detail

Example output:
![SquiggleDraw Example Output](https://cloud.githubusercontent.com/assets/2564583/20510932/dd6f7f7c-b028-11e6-94df-dd1693214b3a.png)

SquiggleDraw UI:
![SquiggleDraw UI](https://cloud.githubusercontent.com/assets/2564583/22277912/50d6a0da-e274-11e6-9605-1838b998aed0.png)