diff --git a/README.md b/README.md index ebe6330..57506cc 100644 --- a/README.md +++ b/README.md @@ -19,28 +19,39 @@ A static start page to get to your most important links, **FAST**. You can use t - Quick link preview - Search (with search provider and tag support) - Docker support -- Local Config management +- Multiple profiles +- Load remote profile +- Caching strategies - PWA support - Keyboard shortcuts +- Read Only mode - Full keyboard navigation support ## Screens Landing page -![Landing Page](docs/assets/screen.png) +| Desktop | Mobile | +| ----------------------------------------------------- | --------------------------------------------------- | +| ![Landing Page Desktop](docs/assets/main-desktop.png) | ![Landing Page Mobile](docs/assets/main-mobile.png) | Quickly find links -![Quickly find links](docs/assets/screen-search.png) +| Desktop | Mobile | +| ------------------------------------------------------- | ----------------------------------------------------- | +| ![Landing Page Desktop](docs/assets/search-desktop.png) | ![Landing Page Mobile](docs/assets/search-mobile.png) | Easily edit links -![Locally edit links](docs/assets/screen-edit.png) +| Desktop | Mobile | +| ----------------------------------------------------- | --------------------------------------------------- | +| ![Landing Page Desktop](docs/assets/edit-desktop.png) | ![Landing Page Mobile](docs/assets/edit-mobile.png) | Locally manage config using JSON -![Locally manage config using JSON](docs/assets/screen-config.png) +| Desktop | Mobile | +| --------------------------------------------------------------- | ------------------------------------------------------------- | +| ![Landing Page Desktop](docs/assets/config-manager-desktop.png) | ![Landing Page Mobile](docs/assets/config-manager-mobile.png) | Drag & drop config file @@ -112,116 +123,60 @@ e.g.: ### Using Config -Since this is a static website, the only way to permanently update the links is to modify the `config.json` file. If using the pre built version, just update the `config.json` file in the release zip. During development, update the config in the `assets` folder since the build will override any other config file. +Since this is a static website, all edits made are local and the new config must uploaded to a server from where the configs can be fetched. To make this process easier, Hiccup comes with a built in config manager. The config manager allows you to: -To update config on a local browser instance, use the online config editor using the ⚙️ icon. This will persist the config across sessions. The local editor allows to: +- Load a config file from either URL or local file +- Manage multiple configs (Preview, Select, Sync, Delete) +- Download config file -- Edit the raw local config -- Sync the configuration from `./configs/config.json` file -- Download the latest valid config file -- Upload a local config file. (psst.. if the file has issues, no problem! The editor will still load it and show you the errors until you can save it) -- Save the config to **LocalStorage** +you can also additionally share hiccup with a preloaded config using the `config` url parameter. -#### Config structure +e.g. -```js -{ - // Uses semantic versioning - "version": "1.0", - // featured and catagories are optional sections. Remove them to use the page without it. - "featured": [{ - "name": "Link name as seen on the card", // required - "link": "link", // required - "background": "path to background image", // optional - "tags": "space spearated tags for searching" //optional - }, { - // ... Other featured links - }], - "catagories": [{ - "name": "Category name", // required - "links": [{ - "name": "Link name as seen on the card", // required - "link": "link", // required - "tags": "space spearated tags for searching" //optional - }, { - // ... Other category links - }] - }, { - // ... Other categorys - }], - "metadata": { - "readonly": false // default - } -} ``` +http://designedbyashw.in/test/hiccup?config=http://your-url.com/config.json +``` + +> Since this is a CORS request, the server should allow requests from the source domain. Github Gists are an easy way to save remote config's + +#### Using with github gists + +To save a config on github gists. + +1. Create the hiccup view you want and download the config. +1. Copy the contents of the file to the gist +1. Click Creat a **Public** Gist +1. Copy the url and add `/raw` to the end. This is now your config URL + +Share the config with anyone to load in their instance of hiccup + +#### Migrations + +If you were using an older version of Hiccup (< v0.4.x), the config manager will automatically recover the old cached config and promt you to save it using the new version. download the recovered config and redistribute it as needed. + +All the config information is stored in **LocalStorage** and never sent to a server anywhere! + +#### Config structure Refer to the [JSON Scheme file](src/modules/validateConfig/schema.json) for the latest schema. -#### Sample config +### Caching + +The app now supports 4 caching strategies, these are useful if you dont want to frequently update your configs or run it completely offine + +Strategies: + +- `cache`: Use only the local cache to save and fetch data +- `network`: Use only the network values and fail if you cannot get it +- `cache-first`: If not found in the cache, fallback to the remote URL +- `network-first`: (Default) If not found in the remote server, fallback to the cached value + +You can set this value using the URL parameter `cache` + +e.g. ``` -{ - "version": "1.0". - "featured": [{ - "name": "Featured Link", - "link": "http://google.com", - "background": "/assets/card.png" - }, { - "name": "Another Feaured link", - "link": "http://google.com" - }, { - "name": "One more", - "link": "http://google.com" - }, { - "name": "Oh no!!!", - "link": "http://google.com", - "tags": "space separated tags" - }], - "categories": [{ - "title": "Category 1", - "links": [{ - "name": "Link 1", - "link": "http://google.com" - }, { - "name": "Link 2", - "link": "http://google.com" - }] - }, { - "title": "Category 2", - "links": [{ - "name": "Link 1", - "link": "http://google.com" - }, { - "name": "Link 2", - "link": "http://google.com" - }] - }, { - "title": "Category 3", - "links": [{ - "name": "Link 1", - "link": "http://google.com", - "tags": "more searchable tags" - }] - }, { - "title": "Category 4", - "links": [{ - "name": "Link 1", - "link": "http://google.com" - }, { - "name": "Link 2", - "link": "http://google.com" - }, { - "name": "Link 3", - "link": "http://google.com" - }, { - "name": "Link 4", - "link": "http://google.com" - }] - }], - "metadata": { - "readonly": false - } -} +http://designedbyashw.in/test/hiccup?cache=network ``` ## Available Scripts for development diff --git a/cypress/e2e/base.cy.ts b/cypress/e2e/base.cy.ts index a234ceb..5c704ce 100644 --- a/cypress/e2e/base.cy.ts +++ b/cypress/e2e/base.cy.ts @@ -14,7 +14,7 @@ describe('Basic tests', () => { .contains('First Default Featured Link') cy.clickSettings() - cy.getConfigPreview('default') + cy.getManagedConfig('default', 'preview') .should('contain.text', 'Default Config') .should('contain.text', 'Active') }) @@ -27,7 +27,7 @@ describe('Basic tests', () => { .contains('This is a dummy config') cy.clickSettings() - cy.getConfigPreview('dummy') + cy.getManagedConfig('dummy', 'preview') .should('contain.text', 'Dummy Config') .should('contain.text', 'Active') }) @@ -55,6 +55,6 @@ describe('Basic tests', () => { cy.clickSettings() - cy.getPreviewingConfig().should('contain', ' - Editing') + cy.getManagedConfig().should('contain', ' - Editing') }) }) diff --git a/cypress/e2e/config.cy.ts b/cypress/e2e/config.cy.ts index 4e3b87c..4b7dc27 100644 --- a/cypress/e2e/config.cy.ts +++ b/cypress/e2e/config.cy.ts @@ -10,21 +10,21 @@ describe('Config Manager', () => { it('should be able to preview configs', () => { cy.findByTestId('file-viewer').find('#id').should('not.contain', 'empty') - cy.getConfigPreview('empty').click() + cy.getManagedConfig('empty').click() cy.findByTestId('file-viewer').find('#id').should('contain', 'empty') }) it('should be able to switch configs', () => { cy.findByTestId('file-viewer').find('#id').should('not.contain', 'empty') - cy.getConfigActivate('empty').click() - cy.getConfigPreview('empty').should('contain.text', 'Active') + cy.getManagedConfig('empty', 'activate').click() + cy.getManagedConfig('empty').should('contain.text', 'Active') }) it('should be able to load remote config', () => { cy.intercept('GET', 'http://dummyconfig.com', { fixture: 'dummy' }) cy.submitUrlInput('http://dummyconfig.com') cy.findByTestId('file-viewer').find('#id').should('contain', 'dummy') - cy.getConfigPreview('dummy').should('contain.text', 'Active') + cy.getManagedConfig('dummy', 'preview').should('contain.text', 'Active') }) it('should be able to sync config', () => { @@ -34,7 +34,7 @@ describe('Config Manager', () => { cy.intercept('GET', '**/configs/config.json', { fixture: 'default-after-sync', }).as('sync') - cy.getConfigSync('default').click() + cy.getManagedConfig('default', 'sync').click() cy.findByTestId('file-viewer') .find('#title') .should('contain', 'Default Synced Config') @@ -44,16 +44,16 @@ describe('Config Manager', () => { cy.intercept('GET', 'http://dummyconfig.com', { fixture: 'dummy' }) cy.submitUrlInput('http://dummyconfig.com') cy.findByTestId('file-viewer').find('#id').should('contain', 'dummy') - cy.getConfigPreview('dummy').should('contain.text', 'Active') + cy.getManagedConfig('dummy', 'preview').should('contain.text', 'Active') // disabled delete default config - cy.getConfigDelete('default').should('be.disabled') - cy.getConfigPreview('default').should('exist') + cy.getManagedConfig('default', 'delete').should('be.disabled') + cy.getManagedConfig('default', 'preview').should('exist') // Delete dummy config - cy.getConfigPreview('dummy').should('exist') - cy.getConfigDelete('dummy').click() - cy.getConfigPreview('dummy').should('not.exist') + cy.getManagedConfig('dummy', 'preview').should('exist') + cy.getManagedConfig('dummy', 'delete').click() + cy.getManagedConfig('dummy', 'preview').should('not.exist') }) it('should download config', () => { diff --git a/cypress/e2e/visual.cy.ts b/cypress/e2e/visual.cy.ts new file mode 100644 index 0000000..d4f84cb --- /dev/null +++ b/cypress/e2e/visual.cy.ts @@ -0,0 +1,58 @@ +// Run this only when you need updated screenshots +// Run on a reasonably large monitor so that scrollbars dont appear. This seesm to be a bug with cypress screenshot +describe.skip('Screenshots', () => { + const screens: { + type: string + orientation?: Cypress.ViewportOrientation + viewport: Cypress.ViewportPreset + }[] = [ + { + type: 'desktop', + viewport: 'macbook-13', + }, + { + type: 'mobile', + viewport: 'iphone-8', + }, + ] + + beforeEach(() => { + // TODO: get app url dynamically + cy.visit('http://localhost:3000') + cy.intercept('GET', '**/configs/config.json', { fixture: 'default' }) + cy.intercept('GET', '**/assets/*').as('image') + }) + + screens.forEach(({ type, viewport, orientation }) => { + it(`should capture all major ${type} screens`, () => { + cy.viewport(viewport, orientation ?? 'portrait') + // Wait for app to load + cy.wait('@image') + cy.get('.toast').should('be.visible') + cy.get('.toast').should('be.hidden') + + // Landing page + cy.screenshotPreviewImage(`main-${type}`) + + // Search + cy.typeInSearchBar('Fea') + cy.screenshotPreviewImage(`search-${type}`) + cy.typeInSearchBar('{esc}') + + // Hotkey edit + cy.blurSearch().type('{cmd+e}') + cy.screenshotPreviewImage(`edit-${type}`) + + // Exit edit mode and hotkey config maager + cy.blurSearch().type('{cmd+e}') + cy.blurSearch().type('{cmd+k}') + cy.screenshotPreviewImage(`config-manager-${type}`) + cy.closeModal() + + // Hotkey modal + cy.get('body').type('{cmd+/}') + cy.screenshotPreviewImage(`hotkey-${type}`) + cy.closeModal() + }) + }) +}) diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 9b3aa33..399dfbc 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -7,11 +7,11 @@ declare namespace Cypress { * @example cy.clickOnMyJourneyInCandidateCabinet() */ clickSettings(): Chainable - getConfigPreview(configId: string): Chainable - getConfigActivate(configId: string): Chainable - getConfigSync(configId: string): Chainable - getConfigDelete(configId: string): Chainable - getPreviewingConfig(): Chainable + closeModal(): Chainable + typeInSearchBar(test: string): Chainable + screenshotPreviewImage(name: string): Chainable + getManagedConfig(id?: string, action?: ConfigAction): Chainable + submitUrlInput(url: string): Chainable getEditLinkModal(): Chainable getCachedConfigs(): Chainable diff --git a/cypress/support/pageCommands.ts b/cypress/support/pageCommands.ts index 57c098a..6841755 100644 --- a/cypress/support/pageCommands.ts +++ b/cypress/support/pageCommands.ts @@ -1,33 +1,55 @@ // Add all the page level commands +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type ConfigAction = 'preview' | 'activate' | 'sync' | 'delete' + +const CONFIG_ACTIONS = { + preview: -1, + activate: 0, + sync: 1, + delete: 2, +} + +// export type ConfigAction = keyof typeof CONFIG_ACTIONS + Cypress.Commands.add('clickSettings', () => { cy.findByTestId('global-settings').click() }) -Cypress.Commands.add('getConfigPreview', (configId: string) => { - cy.findByTestId(`cached-config-${configId}`) +Cypress.Commands.add('closeModal', () => { + cy.findByTestId('close-modal').click() }) -Cypress.Commands.add('getConfigActivate', (configId: string) => { - cy.findByTestId(`cached-config-${configId}`).find('button').eq(0) +Cypress.Commands.add('screenshotPreviewImage', (name) => { + cy.screenshot(`preview/${name}`, { + overwrite: true, + capture: 'viewport', + }) }) -Cypress.Commands.add('getConfigSync', (configId: string) => { - cy.findByTestId(`cached-config-${configId}`).find('button').eq(1) +Cypress.Commands.add('typeInSearchBar', (text) => { + cy.findByTestId('search-bar').type(text) }) -Cypress.Commands.add('getConfigDelete', (configId: string) => { - cy.findByTestId(`cached-config-${configId}`).find('button').eq(2) +Cypress.Commands.add('getManagedConfig', (id, action = 'preview') => { + if (action === 'preview') { + !!id + ? cy.findByTestId(`cached-config-${id}`) + : cy.get('body').find('.previewing') + } else { + const index = CONFIG_ACTIONS[action] + !!id + ? cy.findByTestId(`cached-config-${id}`).find('button').eq(index) + : cy.get('body').find('.previewing button').eq(index) + } }) +// Not happy with these, refactor to be more useful + Cypress.Commands.add('submitUrlInput', (url: string) => { cy.findByTestId('config-url-input').find('input').type(`${url}{Enter}`) }) -Cypress.Commands.add('getPreviewingConfig', () => { - cy.get('body').find('.previewing') -}) - Cypress.Commands.add('getCachedConfigs', () => { cy.get('div[data-testid^="cache"]') }) @@ -37,5 +59,5 @@ Cypress.Commands.add('getEditLinkModal', () => { }) Cypress.Commands.add('blurSearch', () => { - cy.get('body').click(5, 50) + cy.get('body').click(5, 5) }) diff --git a/docs/assets/config-manager-desktop.png b/docs/assets/config-manager-desktop.png new file mode 100644 index 0000000..f966a90 Binary files /dev/null and b/docs/assets/config-manager-desktop.png differ diff --git a/docs/assets/config-manager-mobile.png b/docs/assets/config-manager-mobile.png new file mode 100644 index 0000000..3a230d8 Binary files /dev/null and b/docs/assets/config-manager-mobile.png differ diff --git a/docs/assets/edit-desktop.png b/docs/assets/edit-desktop.png new file mode 100644 index 0000000..3b2d23d Binary files /dev/null and b/docs/assets/edit-desktop.png differ diff --git a/docs/assets/edit-mobile.png b/docs/assets/edit-mobile.png new file mode 100644 index 0000000..58384bb Binary files /dev/null and b/docs/assets/edit-mobile.png differ diff --git a/docs/assets/hotkey-desktop.png b/docs/assets/hotkey-desktop.png new file mode 100644 index 0000000..2844133 Binary files /dev/null and b/docs/assets/hotkey-desktop.png differ diff --git a/docs/assets/hotkey-mobile.png b/docs/assets/hotkey-mobile.png new file mode 100644 index 0000000..61caac0 Binary files /dev/null and b/docs/assets/hotkey-mobile.png differ diff --git a/docs/assets/main-desktop.png b/docs/assets/main-desktop.png new file mode 100644 index 0000000..e3d0ddd Binary files /dev/null and b/docs/assets/main-desktop.png differ diff --git a/docs/assets/main-mobile.png b/docs/assets/main-mobile.png new file mode 100644 index 0000000..4ddb432 Binary files /dev/null and b/docs/assets/main-mobile.png differ diff --git a/docs/assets/search-desktop.png b/docs/assets/search-desktop.png new file mode 100644 index 0000000..5974407 Binary files /dev/null and b/docs/assets/search-desktop.png differ diff --git a/docs/assets/search-mobile.png b/docs/assets/search-mobile.png new file mode 100644 index 0000000..377cb6f Binary files /dev/null and b/docs/assets/search-mobile.png differ diff --git a/package.json b/package.json index f71e9f0..445289c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "cypress:open": "cypress open" + "cypress:open": "cypress open", + "cypress:run": "cypress run --browser chrome" }, "eslintConfig": {}, "browserslist": { diff --git a/src/App.tsx b/src/App.tsx index 522762b..d809adf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,23 @@ const App = () => ( - + diff --git a/src/components/ConfigEditor/StoreScreen.module.css b/src/components/ConfigEditor/StoreScreen.module.css index 10e1300..42e165f 100644 --- a/src/components/ConfigEditor/StoreScreen.module.css +++ b/src/components/ConfigEditor/StoreScreen.module.css @@ -1,11 +1,9 @@ .screen { - height: 800px; - width: 1000px; display: grid; grid-template: 'list list fileViewer' 1fr 'fileLoader download fileViewer' - 'url url url' / 25% 25% 50%; + 'url url url' / minmax(200px, 1fr) minmax(200px, 1fr) minmax(300px, 2fr); padding: 40px; gap: 10px; } @@ -64,6 +62,8 @@ border-radius: 5px; font-family: monospace; margin: 0; + overflow: hidden; + text-overflow: ellipsis; } .lineItem > pre { diff --git a/src/components/ConfigEditor/StoreScreen.tsx b/src/components/ConfigEditor/StoreScreen.tsx index 0845ec2..757aa5b 100644 --- a/src/components/ConfigEditor/StoreScreen.tsx +++ b/src/components/ConfigEditor/StoreScreen.tsx @@ -197,7 +197,6 @@ const IconButton: FC = ({ diff --git a/src/index.css b/src/index.css index 7f503ea..45154ed 100644 --- a/src/index.css +++ b/src/index.css @@ -50,6 +50,13 @@ body::-webkit-scrollbar-thumb { place-items: center; align-content: center; background-color: var(--theme-bg-1); + padding-top: 15px; +} + +@media (max-width: 600px) { + #root { + padding: 0; + } } a {