For 6.5 inch screenshots, boot up an iPhone XS Max.
For 5.5 inch screenshots, boot up an iPhone 8 Plus.
For iPad Pro (12.9) 2nd and 3rd gen, boot up an iPad Pro (12.9-inch) simulator.
For 6.5 inch screenshots, boot up an iPhone XS Max.
For 5.5 inch screenshots, boot up an iPhone 8 Plus.
For iPad Pro (12.9) 2nd and 3rd gen, boot up an iPad Pro (12.9-inch) simulator.
Firebase doesn’t have a nice way to say .where(searchField, ‘startsWith’, searchTerm)
The easiest way is to search for text strings that are greater than the search term, and smaller than the next string after the search term.
E.g. when searching for rows that start with “Rhys”, we really want everything greater or equal than Rhys, and everything less than or equal to RhysZ (to oversimplify).
Here’s a quick and easy way to do that, using \uF8FF (a high unicode code point) as the trailing character.
return usersCollection
.limit(perPage)
.where('email', '>=', searchText)
.where('email', '<=', searchText + '\uF8FF')
.get()
One of those easy-to-forget and hard-to-google things is inlining tags with pug.
template(#modal-title)
| Edit #[i {{editingMenuItem.menuItem.name}} ]
Outputs “<p>Edit <i>text</i></p>
This is one of those things I always forget that’s hard to google properly.
In a child component, adding v-model support starts with a ‘value’ prop. This is the input value arriving at the child component. In the example below, I’m expecting to work with an array.
props: {
value: {
default: () => [],
type: Array,
},
},
We don’t want to mutate the underlying passed in value when it gets changed in the component, so the value prop is cloned into a data property.
data() {
return {
editingRow: false,
sections: [...this.value]
}
},
From here on in, we manipulate only the sections data, never touching the input value prop.
saveSection(){
this.sections.push(this.editingRow);
this.editingRow = false;
}
When it’s time to notify the parent and sync the changes (on clicking save, or just on a practical point within the app), we emit a ‘input’ event with the new contents of the value.
this.$emit('input', this.sections);
Eating at a pub involves lining up with the other patrons, reviewing the menu, speaking back the order and any alterations to staff who enter it in the system and ring up a price.
The customer pays for the order and the order goes to the back-restaurant/make-bench to be made.
A modern approach (ME&U app?) allows the pub to create a restaurant in the app and add their menu items, sorted by categories. Products have a price (alternatively a second price for other purposes, e.g. members). Products have extensive alterations.
The pub prints labels for each desk that have the site code and table code embedded in a QR code for the customer to scan. The business can alternatively print nice labels and have them sent out – the labels attach to a cutlery basket, stick to the table, The customer gets taken to the app with a querystring that can load up the restaurant and the menu.
The customer fills a ‘cart’ on the site and ultimately checks out. The customer needs to make payment when they submit an order. The restaurant can begin cooking the order for the customer, via a POS integration.
If the customer does not understand, gets stuck, or chooses not to use the app, they can still proceed ‘the old way’.
The customer can continue to shop on their app for drinks and order and pay online.
The customer can use the receipt link SMS’ed to their phone for disputes etc.
If payment is through the app, a clip will be paid to “Ordering app”.
The ‘pub’ needs customised labels for tables and an easy way to re-order or re-create their own. All QR codes should be easily reproducible.
The orders only make it to the back kitchen by a POS integration or a receipt printer on site.
The pub has a dashboard they can use, but this is not a key part of their use of the platform (e.g. they don’t need to get orders out of it, etc). They might sign in for refunds and billing enquiries, check payment settlements, update menus (if not POS integrated).
The obvious first step is to get in touch with a restaurant manager or owner and run the idea by them – does it sound like it’ll work?
No more original side projects.
This is an ongoing article of unoriginal side project ideas that could be executed. I don’t know what I’m doing, I’m just documenting this so I can find it again later.
The unoriginal side projects will follow a bit of a formula – having many elements in common. I’ll keep revisiting the article and adding examples.
Certain SaaS products go “Enterprise” or upmarket and leave a hole that can be filled to deal with smaller customers. They end up full of thousands of features, and there may be an underserved market of users who could be paying for a much lighter product.
They end up as “Request a demo” or “get more information”.
On reflecting on the enterprise behemoths below it looks like I’m doing the old “Ha, twitter!? I could write that in a weekend!”. It’s not that. I want to take an enterprise behemoth, slicing it back down to what it used to be, and build that.
Any one will still take me months of nights, realistically. I’ve got a full time job and a big family that both take priority, but also a strong desire to stop trading hours for money.
I’ll build up a stack I can re-use, find some valid and invalid assumptions, ship some products, and keep moving the goalposts as it goes.
The target market for any of these projects will be a person with an online or offline business who already knows they need a <SaaS for X> (where X is “Trust widget”, “Support ticket tool”, …).
I don’t want to convince them they need one – I suck at that. I just need to convince them that this will also generate support tickets, doing much the same shit as Zendesk. They’re not going to find me in position 1 on google – that’ll always be Uservoice. They’re going to find me on page 2 of google (after abandoning all the “Schedule a demo!” shit), read a few articles, watch a video and create an account on the spot. Sometimes, hopefully.
Follows is my incomplete list of up-market enterprise SAML HIPAA compliant “schedule a call with our executive integration team” bullshit unicorns that I think have an underserved lower end of the market.
Ideally, I’d pad this list out with some smaller stuff, but hey, I’m starting here.
Customers email [email protected] and start a conversation thread with one or multiple support agents.
Customers use live chat
A knowledgebase is slowly created, which becomes SEO long tail bait, reference material, internal bible, and inbound lead generation.
Zendesk $5/agent/mo to $19/agent/mo (Max $99/agent/mo).
HelpScout $20/agent/mo to $35/agent/mo
Customers visit a feature request leaderboard, and it helps drive feature development of the SaaS.
In theory helps drive development roadmap. Gives users a place to help each other, self diagnose, request features.
uservoice is the big one ($499+/mo.). Loop, roadmap.space, etc are others.
Users can fill out a form, and the data is shipped to the form owner or a google sheet.
The form has many field types, including payments, maps, etc. New field types can be added regularly.
Paperform (12.50/mo to 32.50/mo, up to 82.50/mo)
Users fill data into spreadsheets, either with forms or spreadsheet view. They can link the sheets together, export to CSV, use as REST API.
Airtable $10-$20/user/mo. Airtable is a $X00B company, but 99% of the usefulness (that I can see) is linked spreadsheet rows that you can share with co-workers and devs.
When an order is shipped, the trust server sends an email to the customer in a few days asking them to review the products and review the website.
The customer can review both and the data flows both to the website and to a list of the products.
Trustpilot $189/mo
Award points and credits for purchases to a customers account. Customer can spend the points and credits on checkout.
loyaltylion: $159/mo
It’s a long title. I want to execute more than one unoriginal side project, so as many factors should be shared as possible, as easily as possible.
I’m not making original side projects anymore – I’m hunting for financially viable ideas that *have already been executed*.
I’ll cut down the scope and clone them as quickly as possible, and set them free. Each project will get sharper, build on the tools from the last, and eventually they will start finding a base.
Here’s a list of things that all of my unoriginal side projects will have.
I had a thought tonight while out for a walk. If I wanted to open a takeaway shop, I’d start keeping track of the ones that looked like they were going well, and basically copy the execution model.
A coffee shop, a burger shop, or a bakery. Maybe a sushi store. There’d be no real ultra unique secret sauce typically, and I sure as hell wouldn’t spend years of my life waiting for the right type of takeaway shop to appear in my mind.
I’m not sure why I’ve spent years coming up with an original idea for a side project, executing it quickly, trying to work out a sales ‘message’, and doing the slow grind to find users. It’s just buying lottery tickets and hoping for the best.
I built “WooToApp”, an app builder for ecommerce stores. It didn’t exist at the end of the market that I wanted to target.
There were HUGE technical limitations and problems, and I felt like I was really innovating when architecting secure payments, secure orders, and a true drag and drop mobile app store builder for ecommerce stores.
I spent so long fucking around and working on techniques. Writing a piece of code and finding issues and re-writing.
Some technical limitations I just didn’t have enough time to reliably overcome. How can a user sign up instantly and have the app on their phone? I spent, I think, hundreds of hours on this.
I never killed it off. Just stopped iterating one day and it slowly died itself, never returning a dollar.
I built “Elvenda” with my brother in law, a dropshipping enabler. Supplier signs up and adds their product catalog. Dropshipper signs up and imports products. Happy days, we can take a clip!
Except for all the problems that we had to discover, etch out solutions and execute ourselves. With no revenue or help, and no time.
What happens if fake products get shipped? How do returns happen? What platforms should we support? What formats can you import from? How the hell do we sell the idea to a “real” supplier?
We haven’t killed this off, and likely won’t. There’s just so much to do.
In 2016, I did some integration work for a brand new payment gateway called “AfterPay”. It didn’t support my clients store platform. The PDF they supplied as API docs was pretty rough and incomplete.
I quoted 12 hours to integrate AfterPay in this guys store and busted my ass. His sales increased overnight. Within a few weeks, 30% of his orders were using this new payment method, and his average order value was far higher. It was stupid expensive too, he was paying 5.5% + 30c on transactions.
The PDF documentation was so bad that by the time the integration was finished, I’d reverse engineered half of the API with postman. I got it.
I decided there’s nothing to it, and took 2 weeks off from contracting and wrote an almost exact clone of everything I could see. All it did was charge the end customer with Stripe on a cron job, and send me an email telling me how much to pay the merchant the next day.
I approached a client and also a friend with ecommerce stores and asked them to add my payment gateway, “<name removed>”.
Within weeks, the gateway was turning over so much money that my wife and I were out of cash. It was the most stressful 6 months of my life, but the nasty 2 week clone was working.
It works like this: A customer of the ecommerce store paid with ‘<name removed>’ for their $100 purchase. We paid the ecommerce store $94.20 ($100 – 5.5% – 30c), and basically hoped like hell that we’d recover the $100 from the customers credit card. The payment comes out in 4 payments over the next 8 weeks – it’s an addictive model, and .. Everybody knows about AfterPay now. They didn’t then.
Some guesswork modeling suggested 2% of people would be bad actors. I did some modeling on late fees and return customers as well that I don’t remember. We didn’t have any machine learning, historical data, AI or identity verification like AfterPay did. I just assumed everyone was a good actor, and banned their mobile number if they weren’t.
Since my phone was full of transactions every morning, I added a postcode to the transaction email. If we had more than 2 bad actors from a postcode the whole area would get banned. “Sorry, <name removed> is unavailable right now”.
As an aside, I remember a “Fuck Warrnambool” comment in the codebase – people in rural Victoria were somehow ripping me off.
We had no money, and our best guess was that <name removed> was making money. At any point in time if we added up the money that was out with merchants (that we were waiting to collect from their customers), deducted 2% for bad actors, it looked like it was creeping upwards ever so slowly.
I had $20,000 of my family’s money in <name removed>, and I’ve never been so stressed in all my life. I finally gave in one day – I couldn’t afford to grow it (I had $0 in real terms in the bank!) and had no idea how to take on money. I wasn’t having any luck with family and friends, and really didn’t even know how to ask.
In January 2017, we flicked the switch and turned off <name removed>. Customers could not check out with it anymore. The $20,000 we’d invested in <name removed> 6 months earlier slowly dripped back in and we ended up with $30,000. I sold the codebase to somebody who is still operating the business.
<name removed> worked, and was working. There was absolutely 0 original ideas and insight. I reverse engineered the plugin, had a best guess at how the backend worked, made the same or similar flows and went and got a customer.
There was no tricky decisions. No architecting flows. No guessing how to handle refunds. Not sure? Check AfterPay. Need a merchant agreement? Check AfterPay. What do I need to tell customers when a transaction is successful? Check AfterPay.
I don’t want to build Tesla or Uber, carving out a unique piece of history and making a huge dent on the world. I want Friday lunch at the beach and sleep ins on Monday.
I’m not doing original ideas anymore. You want to start a takeaway shop? You buy a coffee machine, a cash register, get a coke fridge and find a supplier of muffins.
You don’t scour the world looking for original ideas or try to come up with a meat free hamburger in your spare time. Nobody would buy it.
Next time, I’m picking something with a revenue model, that clearly makes money, that people have heard about, that is overpriced.
I’ll chop it down, make it cheap, easy to sign in and use, and easy for me to sell.
It’s not going to be technically interesting, ground-breaking, and there’s going to be no complex decisions involved.
I’m finding the boring profitable downtown coffee shop of SaaS tools and opening another one down the road.
As part of a long effort to de-google where practical, I’ve moved away from Gmail to Hey.com email service.
The problem with decoupling anything online from Google is twofold –
1. all of your stuff already lives there – I’ve had a Gmail account for 16 years. It’s all perfectly designed to keep you in the flywheel, letting it capture your information.
2. the services are best of breed. Gmail is pretty good. Fast, works everywhere, filters nicely into subcategories.
I have no idea how to get the last 16 years of data out, in a searchable, indexed, private way. That’s for another day. This post is about futureproofing the transition to Hey.
The temptation to just set up an auto forward from Gmail to Hey won’t fix the problem. You won’t update your email address, your personal information won’t be untethered from Google, and you’ll be paying the Hey folks $99 a year for a pretty thin email client.
Do it this way instead:
In Settings > See all Settings > Labels, create a new label “To Be Ported”.
In Settings > “Filters and blocked addresses”, create a new filter. If it was sent to you, skip the inbox and move it to “To Be Ported”.
This takes forever. Every single email you receive is either shit you don’t care about, something you should update, something you can’t update, or something that’s not worth updating yet.
Next to the new label “To Be Ported”, click the 3 dots and add 4 new sub labels. Cannot port, Irrelevant, Ported, Unsubscribed.
All the mailing list subscriptions you’ve accumulated over the last 10+ years – open them one by one as they appear in the “To Be Ported” label, unsubscribe from them, and move them to the sub-label called ‘Unsubscribed’. That’s 90% of them solved by quantity.
About half of these will allow you to click ‘Update preferences’ near the bottom of the email and change your email address on file. Of these, about half will actually work.
Generally, you’ll need to unsubscribe and then resubscribe with your new Hey.com email address.
Move all these emails to the “Ported” label – just drag them.
I have no idea if I can port my icloud identity away from my gmail account. Didn’t try, not worth the hassle. Also, the emails they send me are junk. Drag these to the “Cannot Port” label. The only use for this is keeping the “To Be Ported” label empty, so you can deal with emails as they arrive.
My wife will probably never update my email contact. I could get around to sorting that out, but I won’t. I drop those emails in here since they are important to me but not ported.
I’m getting there. Once the ‘To Be Ported’ inbox slows to a dribble of new emails, it’s time for you to set up that auto forwarder. This means when a random old contact from 5 years ago emails you, it’ll go straight to the Hey.com imbox.
Create a new filter (Settings > “Filters and blocked addresses”) and send everything on over to the Hey.com address.
Not sure of this yet. Enjoy your new email!
A simple firebase http function template:
// accessible at https://us-central1-<app name>.cloudfunctions.net/testEndpoint
exports.testEndpoint = functions.https.onRequest(req, res) => {
if (cors(req, res)) {
res.end()
return
}
// do something
// make sure to 'end' the request or it'll time out
res.send(true)
res.end()
})
We need to handle CORS requests (see cors()
above), because the browser will send an OPTIONS request first to see what’s accepted by the endpoint. Your function handler in this case will receive an empty body, throw a bunch of errors, and generally waste a ton of time.
A simple CORS handler template:
const cors = (req, res) => {
const ALLOWED_ORIGINS = [
'http://localhost:3000',
'https://app.domain.com'
]
if (ALLOWED_ORIGINS.includes(req.headers.origin)) {
res.set('Access-Control-Allow-Origin', req.headers.origin)
res.set('Access-Control-Allow-Headers', '*')
}
if (req.method === 'OPTIONS') {
res.end()
return true
}
return false
}
If it’s an OPTIONS request, send back the approved origin and end the response (via true response).
If it’s a different type of request, set the approved origin and continue (via false response).