├── package.json ├── .gitignore ├── ui-behaviour-tests.js ├── ui-style.css ├── README.md ├── identity.svg ├── ui-behaviour.js ├── index.html ├── ui-test-_access.js └── ui-test-_misc.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "olsk-spec" 4 | }, 5 | "devDependencies": { 6 | "OLSKSpec": "olsk/OLSKSpec" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NODE 2 | node_modules 3 | package-lock.json 4 | 5 | # BUILD 6 | **/__*/* 7 | .static 8 | 9 | # OS 10 | **/*.DS_Store 11 | 12 | # LOCAL 13 | .env* 14 | !.env-sample 15 | -------------------------------------------------------------------------------- /ui-behaviour-tests.js: -------------------------------------------------------------------------------- 1 | const { throws, rejects, deepEqual } = require('assert'); 2 | 3 | const mod = require('./ui-behaviour.js'); 4 | 5 | describe('APRVitrineRandomAnchor', function test_APRVitrineRandomAnchor() { 6 | 7 | it('returns string', function () { 8 | deepEqual(mod.APRVitrineRandomAnchor(), 'random'); 9 | }); 10 | 11 | }); 12 | 13 | describe('APRVitrineRefreshSeconds', function test_APRVitrineRefreshSeconds() { 14 | 15 | it('returns string', function () { 16 | deepEqual(mod.APRVitrineRefreshSeconds(), 3); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /ui-style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: sans-serif; 3 | font-size: 10pt; 4 | 5 | --APRTextColor: #555; 6 | --APRLinkColor: black; 7 | --APRPadding: 10px; 8 | } 9 | 10 | html { 11 | height: 100%; 12 | } 13 | 14 | body { 15 | display: flex; 16 | height: 100%; 17 | padding: 0; 18 | 19 | margin: 0; 20 | 21 | background: #f4f4f4; 22 | color: var(--APRTextColor); 23 | } 24 | 25 | a { 26 | color: var(--APRLinkColor); 27 | } 28 | 29 | hr { 30 | border: 1px solid white; 31 | } 32 | 33 | .APRBox { 34 | flex-grow: 1; 35 | padding: var(--APRPadding); 36 | border-radius: 2px; 37 | 38 | margin: var(--APRPadding); 39 | 40 | background: #ddd; 41 | 42 | background-image: url(identity.svg); 43 | background-size: 42px; 44 | background-repeat: no-repeat; 45 | background-position: bottom 10px right 10px; 46 | } 47 | 48 | .APRVitrine p { 49 | max-width: 400px; 50 | } 51 | 52 | .APRVitrineList { 53 | padding-left: calc(var(--APRPadding) * 3); 54 | } 55 | 56 | .APRRandomTargetName { 57 | font-weight: bold; 58 | } 59 | 60 | .APRRandomTargetBlurb { 61 | margin: 0; 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project logo 2 | 3 | # [Doorless App Ring](https://ring.0data.app) 4 | 5 | _A webring for web apps_ 6 | 7 | This appring supports [doorless](https://rosano.hmm.garden/01evv3hq1ak4b6ng1jzppx5n2j) projects that are functional without signing into an account. 8 | 9 | https://ring.0data.app 10 | 11 | It is not necessary for it to be [0data](https://0data.app) or a [Progressive web app](https://en.wikipedia.org/wiki/Progressive_web_application), but these would be ideal. Doorless is another way of saying 'someone can show up and start using it immediately'. 12 | 13 | Inspired by [XXIIVV/webring](https://github.com/XXIIVV/webring) and [indiewebring](https://indieweb.org/indiewebring). 14 | 15 | ## Add your project(s) to the ring 16 | 17 | 1. Link to the appring from your project's homepage or inside the app itself (see below for example code that you can copy and modify). 18 | 2. Add your project URL to [index.html](https://github.com/0dataapp/lap/edit/master/index.html) 19 | 3. Submit a Pull Request with the location of the appring link in your project. 20 | 21 | ### Link via text 22 | 23 | ```html 24 | Part of the Appring 25 | ``` 26 | 27 | ### Link via image 28 | 29 | ```html 30 | 31 | ``` 32 | 33 | ### Link via image and text 34 | 35 | ```html 36 | Part of the Appring 37 | ``` 38 | 39 | ## ❤️ 40 | 41 | Help me keep creating projects that are public, accessible for free, and open-source. 42 | 43 | Send a gift 44 | 45 | ## Questions 46 | 47 | Feel free to reach out on [Mastodon](https://rosano.ca/mastodon) or [Bluesky](https://rosano.ca/bluesky). 48 | -------------------------------------------------------------------------------- /identity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ui-behaviour.js: -------------------------------------------------------------------------------- 1 | const uRandomElement = function () { 2 | const array = [].concat(...arguments); 3 | return array[Date.now() % array.length]; 4 | }; 5 | 6 | const mod = { 7 | 8 | OLSKControllerRoutes () { 9 | return { 10 | APRVitrineRoute: { 11 | OLSKRoutePath: '/', 12 | OLSKRouteMethod: 'get', 13 | OLSKRouteFunction (req, res, next) { 14 | return res.render(require('path').join(__dirname, 'index.html')); 15 | }, 16 | }, 17 | APRRandomRoute: { 18 | OLSKRoutePath: '/#' + mod.APRVitrineRandomAnchor(), 19 | OLSKRouteMethod: 'get', 20 | OLSKRouteFunction (req, res, next) { 21 | return res.render(require('path').join(__dirname, 'index.html')); 22 | }, 23 | }, 24 | }; 25 | }, 26 | 27 | APRVitrineRandomAnchor () { 28 | return 'random'; 29 | }, 30 | 31 | APRVitrineRefreshSeconds () { 32 | return 3; 33 | }, 34 | 35 | // DATA 36 | 37 | DataProjects () { 38 | return Array.from(mod._APRVitrine.querySelectorAll('li a')).map(function (e) { 39 | return { 40 | APRProjectName: e.innerHTML, 41 | APRProjectURL: e.getAttribute('href'), 42 | APRProjectBlurb: e.getAttribute('title'), 43 | }; 44 | }); 45 | }, 46 | 47 | // MESSAGE 48 | 49 | WindowHashDidChange () { 50 | mod.SetupRandom(); 51 | }, 52 | 53 | // SETUP 54 | 55 | _SetupMethods () { 56 | return Object.keys(mod).filter(function (e) { 57 | return e.match(/^Setup/); 58 | }); 59 | }, 60 | 61 | SetupElements () { 62 | mod._APRVitrine = document.querySelector('.APRVitrine'); 63 | mod._APRRandom = document.querySelector('.APRRandom'); 64 | }, 65 | 66 | SetupRandom () { 67 | const isRandom = window.location.hash.replace(/^#+/, '').trim() === mod.APRVitrineRandomAnchor(); 68 | 69 | document.body.removeChild(isRandom ? mod._APRVitrine : mod._APRRandom); 70 | document.body.appendChild(isRandom ? mod._APRRandom : mod._APRVitrine); 71 | 72 | if (!isRandom) { 73 | return; 74 | } 75 | 76 | const item = uRandomElement(mod.DataProjects()); 77 | 78 | document.querySelector('.APRRandomTargetDomain').innerText = (new URL(item.APRProjectURL)).hostname; 79 | document.querySelector('.APRRandomTargetName').innerText = item.APRProjectName; 80 | document.querySelector('.APRRandomTargetBlurb').innerText = item.APRProjectBlurb; 81 | 82 | const meta = document.createElement('meta'); 83 | meta.httpEquiv = 'refresh'; 84 | meta.content = `${ mod.APRVitrineRefreshSeconds() }; url=${ item.APRProjectURL }`; 85 | mod._APRRandom.appendChild(meta); 86 | }, 87 | 88 | SetupWindowHashChange() { 89 | window.addEventListener('hashchange', mod.WindowHashDidChange, false); 90 | }, 91 | 92 | // LIFECYCLE 93 | 94 | LifecyclePageDidLoad () { 95 | return mod._SetupMethods().forEach(function (e) { 96 | return mod[e](); 97 | }); 98 | }, 99 | 100 | }; 101 | 102 | if (typeof module !== 'undefined') { 103 | module.exports = mod; 104 | } 105 | 106 | const APRVitrineBehaviour = mod; 107 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Doorless App Ring 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
    21 | 22 |
  1. Hyperdraft
  2. 23 |
  3. Moon Forge
  4. 24 |
  5. Pimeja
  6. 25 |
  7. Joybox
  8. 26 |
  9. Zero Data App
  10. 27 |
  11. Easy Indie
  12. 28 |
  13. Launchlet
  14. 29 |
  15. Diffuse
  16. 30 | 31 |
32 | 33 |
34 | 35 |

This webring supports doorless projects that function without signing into an account. The ring welcomes hand-crafted tools, apps, or websites where 'someone can show up and start using it immediately'.

36 | 37 |

To add your project(s) to the ring, link to here from your project and submit a Pull Request.

38 | 39 | Random 40 | 41 | | 42 | 43 | More info 44 | 45 |
46 | 47 |
48 | 49 |

50 | Doorless App Ring 51 | — 52 | A webring for web apps 53 |

54 | 55 |
56 | 57 |

58 | Redirecting to 59 | 60 |

61 |
62 |
63 |
64 |

65 | 66 |
67 | 68 | Directory 69 | 70 |
71 | 72 | 79 | 80 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /ui-test-_access.js: -------------------------------------------------------------------------------- 1 | const kDefaultRoute = require('./ui-behaviour.js').OLSKControllerRoutes().APRVitrineRoute; 2 | 3 | Object.entries({ 4 | APRVitrine: '.APRVitrine', 5 | 6 | APRVitrineList: '.APRVitrineList', 7 | 8 | APRVitrineBlurb: '.APRVitrineBlurb', 9 | 10 | APRVitrineJoin: '.APRVitrineJoin', 11 | 12 | APRVitrineRandomLink: '.APRVitrineRandomLink', 13 | APRVitrineInfoLink: '.APRVitrineInfoLink', 14 | 15 | APRRandom: '.APRRandom', 16 | 17 | APRRandomTitle: '.APRRandomTitle', 18 | APRRandomBlurb: '.APRRandomBlurb', 19 | APRRandomRedirecting: '.APRRandomRedirecting', 20 | APRRandomTargetDomain: '.APRRandomTargetDomain', 21 | APRRandomTargetName: '.APRRandomTargetName', 22 | APRRandomTargetBlurb: '.APRRandomTargetBlurb', 23 | APRRandomRefresh: 'meta[http-equiv="refresh"]', 24 | 25 | APRRandomDirectoryLink: '.APRRandomDirectoryLink', 26 | }).map(function (e) { 27 | return global[e.shift()] = e.pop(); 28 | }); 29 | 30 | describe('APRVitrine_Access', function () { 31 | 32 | before(function() { 33 | return browser.OLSKVisit(kDefaultRoute); 34 | }); 35 | 36 | it('shows APRVitrine', function() { 37 | return browser.assert.elements(APRVitrine, 1); 38 | }); 39 | 40 | it('shows APRVitrineList', function () { 41 | return browser.assert.elements(APRVitrineList, 1); 42 | }); 43 | 44 | it('shows APRVitrineBlurb', function () { 45 | return browser.assert.elements(APRVitrineBlurb, 1); 46 | }); 47 | 48 | it('shows APRVitrineJoin', function () { 49 | return browser.assert.elements(APRVitrineJoin, 1); 50 | }); 51 | 52 | it('shows APRVitrineRandomLink', function () { 53 | return browser.assert.elements(APRVitrineRandomLink, 1); 54 | }); 55 | 56 | it('shows APRVitrineInfoLink', function () { 57 | return browser.assert.elements(APRVitrineInfoLink, 1); 58 | }); 59 | 60 | it('hides APRRandom', function () { 61 | return browser.assert.elements(APRRandom, 0); 62 | }); 63 | 64 | context('APRRandomRoute', function () { 65 | 66 | before(function() { 67 | return browser.OLSKVisit(require('./ui-behaviour.js').OLSKControllerRoutes().APRRandomRoute); 68 | }); 69 | 70 | it('hides APRVitrine', function() { 71 | return browser.assert.elements(APRVitrine, 0); 72 | }); 73 | 74 | it('shows APRRandom', function () { 75 | return browser.assert.elements(APRRandom, 1); 76 | }); 77 | 78 | it('shows APRRandomTitle', function () { 79 | return browser.assert.elements(APRRandomTitle, 1); 80 | }); 81 | 82 | it('shows APRRandomBlurb', function () { 83 | return browser.assert.elements(APRRandomBlurb, 1); 84 | }); 85 | 86 | it('shows APRRandomRedirecting', function () { 87 | return browser.assert.elements(APRRandomRedirecting, 1); 88 | }); 89 | 90 | it('shows APRRandomTargetDomain', function () { 91 | return browser.assert.elements(APRRandomTargetDomain, 1); 92 | }); 93 | 94 | it('shows APRRandomTargetName', function () { 95 | return browser.assert.elements(APRRandomTargetName, 1); 96 | }); 97 | 98 | it('shows APRRandomTargetBlurb', function () { 99 | return browser.assert.elements(APRRandomTargetBlurb, 1); 100 | }); 101 | 102 | it('shows APRRandomRefresh', function () { 103 | return browser.assert.elements(APRRandomRefresh, 1); 104 | }); 105 | 106 | it('shows APRRandomDirectoryLink', function () { 107 | return browser.assert.elements(APRRandomDirectoryLink, 1); 108 | }); 109 | 110 | }); 111 | 112 | }); 113 | -------------------------------------------------------------------------------- /ui-test-_misc.js: -------------------------------------------------------------------------------- 1 | const mod = require('./ui-behaviour.js'); 2 | 3 | const kDefaultRoute = mod.OLSKControllerRoutes().APRVitrineRoute; 4 | 5 | describe('APRVitrine_Misc', function () { 6 | 7 | before(function() { 8 | return browser.OLSKVisit(kDefaultRoute); 9 | }); 10 | 11 | it('sets lang', function() { 12 | return browser.assert.attribute('html', 'lang', 'en'); 13 | }); 14 | 15 | it('sets encoding', function () { 16 | return browser.assert.attribute('meta[http-equiv="Content-Type"]', 'content', 'text/html; charset=utf-8'); 17 | }); 18 | 19 | it('sets width', function () { 20 | return browser.assert.attribute('meta[name="viewport"]', 'content', 'width=device-width'); 21 | }); 22 | 23 | it('sets og:image', function () { 24 | return browser.assert.attribute('link[rel="apple-touch-icon"]', 'href', 'https://static.rosano.ca/swar/touch.png'); 25 | }); 26 | 27 | it('sets og:image', function () { 28 | return browser.assert.attribute('meta[property="og:image"]', 'content', 'https://static.rosano.ca/swar/touch.png'); 29 | }); 30 | 31 | it('sets title', function() { 32 | return browser.assert.text('title', 'Doorless App Ring'); 33 | }); 34 | 35 | describe('APRVitrineRandomLink', function test_APRVitrineRandomLink () { 36 | 37 | it('sets text', function () { 38 | return browser.assert.text(APRVitrineRandomLink, 'Random'); 39 | }); 40 | 41 | it('sets href', function () { 42 | return browser.assert.attribute(APRVitrineRandomLink, 'href', '#' + mod.APRVitrineRandomAnchor()); 43 | }); 44 | 45 | it('classes APRNoScriptHide', function () { 46 | return browser.assert.hasClass(APRVitrineRandomLink, 'APRNoScriptHide'); 47 | }); 48 | 49 | }); 50 | 51 | describe('APRVitrineInfoLink', function test_APRVitrineInfoLink () { 52 | 53 | it('sets text', function () { 54 | return browser.assert.text(APRVitrineInfoLink, 'More info'); 55 | }); 56 | 57 | it('sets href', function () { 58 | return browser.assert.attribute(APRVitrineInfoLink, 'href', 'https://github.com/0dataapp/small-web-app-ring'); 59 | }); 60 | 61 | }); 62 | 63 | context('APRRandomRoute', function () { 64 | 65 | before(function() { 66 | return browser.OLSKVisit(mod.OLSKControllerRoutes().APRRandomRoute); 67 | }); 68 | 69 | describe('APRRandom', function test_APRRandom () { 70 | 71 | it('classes APRNoScriptHide', function () { 72 | return browser.assert.hasClass(APRRandom, 'APRNoScriptHide'); 73 | }); 74 | 75 | }); 76 | 77 | describe('APRRandomTitle', function test_APRRandomTitle () { 78 | 79 | it('sets text', function () { 80 | return browser.assert.text(APRRandomTitle, 'Doorless App Ring'); 81 | }); 82 | 83 | }); 84 | 85 | describe('APRRandomBlurb', function test_APRRandomBlurb () { 86 | 87 | it('sets text', function () { 88 | return browser.assert.text(APRRandomBlurb, 'A webring for web apps'); 89 | }); 90 | 91 | }); 92 | 93 | describe('APRRandomRedirecting', function test_APRRandomRedirecting () { 94 | 95 | it('sets text', function () { 96 | return browser.assert.text(APRRandomRedirecting, 'Redirecting to'); 97 | }); 98 | 99 | }); 100 | 101 | describe('APRRandomDirectoryLink', function test_APRRandomDirectoryLink () { 102 | 103 | it('sets text', function () { 104 | return browser.assert.text(APRRandomDirectoryLink, 'Directory'); 105 | }); 106 | 107 | it('sets href', function () { 108 | return browser.assert.attribute(APRRandomDirectoryLink, 'href', '#'); 109 | }); 110 | 111 | }); 112 | 113 | }); 114 | 115 | }); 116 | --------------------------------------------------------------------------------