Merge branch 'integration' into multiple-post-authors

# Conflicts:
#	gatsby-config.js
#	src/components/post-card/post-card.js
#	src/components/post-view/post-metadata/post-metadata.js
#	src/data/unicorns.json
This commit is contained in:
Corbin Crutchley
2019-09-11 11:15:12 -07:00
106 changed files with 6305 additions and 3531 deletions

8
.travis.yml Normal file
View File

@@ -0,0 +1,8 @@
sudo: false
language: node_js
node_js:
- '10'
cache: npm
script:
- npm test
- npm run build

78
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,78 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team in [our Discord](https://discord.gg/FMcvc6T)
(tag the @unicorn-umpire role, they're the admins/team members). All complaints
will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@@ -7,10 +7,15 @@
<div align="center">
[![Join chat on Discord](https://badgen.net/badge/discord/join%20chat/7289DA?icon=discord)](https://discord.gg/FMcvc6T)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)
Master branch status: [![Master Branch Build Status](https://travis-ci.org/unicorn-utterances/unicorn-utterances.svg?branch=master)](https://travis-ci.org/unicorn-utterances/unicorn-utterances)
Integration branch status: [![Integration Branch Build Status](https://travis-ci.org/unicorn-utterances/unicorn-utterances.svg?branch=integration)](https://travis-ci.org/unicorn-utterances/unicorn-utterances)
</div>
This repository acts as the source code location for the Unicorn Utterances blog found [here](https://unicorn-utterances.com)
This repository acts as the source code location for [the Unicorn Utterances blog](https://unicorn-utterances.com)
## Important Files
@@ -19,10 +24,20 @@ This repository acts as the source code location for the Unicorn Utterances blog
Should be located under [`content/blog/post-name-here`](./content/blog/). You should then have an `index.md` file containing a frontmatter (with JS header, not YAML) portion and any related files should be in the same folder.
### Author Data File
The author data file is located at [`src/data/authors.json`](./src/data/unicorns.json). To add yourself as an author in a PR for a new post, you'd add your information as a new JSON object in the array, then add a profile picture to the `data` folder. The `pronouns` field should match an `id` in the `pronouns.json` (if yours is not listed, please add it as a new value in that file, we've tried to do our best to include everything we've found!)
The author data file is located at [`src/data/unicorns.json`](./src/data/unicorns.json) 🦄
To add yourself as an author in a PR for a new post, you'd add your information as a new JSON object in the array, then add a profile picture to the `data` folder. The `pronouns` field should match an `id` in the `pronouns.json` (if yours is not listed, please add it as a new value in that file, we've tried to do our best to include everything we've found!)
> If you do not want to show a profile picture or commit your picture to the repo, we have a [myriad of emotes that can be used as profile pictures as well](./content/assets/branding/emotes). They're adorable, go check! 🤩
## 🚀 Develop
To start the develop server, run `npm run develop`, it will then start the local instance at `http://localhost:8000`. You also have the ability to checkout the GraphiQL tool at `http://localhost:8000/___graphql`. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql).
## Git Strategy
We loosely follow [the Gitflow branching strategy](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) where our development branch is called `integration` and our mainline branch is called `master`.
This means that any new pull requests should be made against `integration` unless it is an emergency hotfix (to be approved by the devops team).
We also have the `master` branch which is a live reflection of the code hosted on the server. Any time `integration` is pulled into `master`, the site will be deployed. A PR from `integration` to `master` should only be opened by a Unicorn Utterances team member and must be approved by at least one devops member

View File

@@ -0,0 +1,7 @@
export const MockLicense = {
licenceType: 'Mocked License',
footerImg: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png",
explainLink: "example.com/explainLink",
name: "The Mocking License",
displayName: "MockLicense with Attribution"
}

View File

@@ -0,0 +1,22 @@
import { MockUnicorn } from "./mock-unicorn"
import { MockLicense } from "./mock-license"
export const MockPost = {
id: '123123',
excerpt: 'This would be an auto generated excerpt of the post in particular',
html: "<div>Hey there</div>",
frontmatter: {
title: "Post title",
published: '10-10-2010',
tags: ['item1'],
description: 'This is a short description dunno why this would be this short',
author: MockUnicorn,
license: MockLicense
},
fields: {
slug: "/this-post-name-here"
},
wordCount: {
words: 10000
}
}

View File

@@ -0,0 +1,4 @@
export const MockRole = {
id: "developer",
prettyname: "Developer"
}

View File

@@ -0,0 +1,7 @@
export const siteMetadata = {
title: 'siteTitle',
siteUrl: "https://example.com/siteUrl/",
disqusShortname: "disqus-example-shorthand",
repoPath: 'unicorn-example-repo-path',
relativeToPosts: 'relative/to/posts'
}

View File

@@ -0,0 +1,37 @@
import { MockRole } from "./mock-role"
export const MockUnicorn = {
name: "Joe",
id: "joe",
description: "Exists",
color: "red",
fields: {
isAuthor: true
},
roles: [MockRole],
socials: {
twitter: "twtrusrname",
github: "ghusrname",
website: "example.com"
},
pronouns: {
they: "they",
them: "them",
their: "their",
theirs: "theirs",
themselves: "themselves",
},
profileImg: {
childImageSharp: {
smallPic: {
fixed: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
},
mediumPic: {
fixed: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
},
bigPic: {
fixed: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
}
}
}
}

View File

@@ -1,27 +0,0 @@
const React = require("react")
const gatsby = jest.requireActual("gatsby")
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
// these props are invalid for an `a` tag
({
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
ref,
replace,
to,
...rest
}) =>
React.createElement("a", {
...rest,
href: to,
})
),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn(),
}

View File

@@ -0,0 +1,7 @@
import React from 'react'
jest.mock('disqus-react', () => {
return {
DiscussionEmbed: () => <></>
}
});

View File

@@ -0,0 +1,12 @@
import React from "react"
jest.mock('gatsby-image', () => {
return (props) => {
return <img
src={props.fixed}
alt={props.alt}
data-testid={props['data-testid']}
className={props.className}
/>;
}
});

View File

@@ -0,0 +1,15 @@
import React from 'react'
import {onLinkClick} from 'gatsby-plugin-google-analytics';
afterEach(() => {
onLinkClick.mockReset();
})
jest.mock('gatsby-plugin-google-analytics', () => {
const onLinkClick = jest.fn();
return {
OutboundLink: (props) => <div onClick={onLinkClick}>{props.children}</div>,
onLinkClick
}
});

View File

@@ -0,0 +1,43 @@
const React = require("react")
import {onLinkClick} from 'gatsby';
afterEach(() => {
onLinkClick.mockReset();
})
jest.mock('gatsby', () => {
const react = require('react');
const gatsbyOGl = jest.requireActual('gatsby');
const onLinkClick = jest.fn();
return {
...gatsbyOGl,
Link: react.forwardRef((props, ref) => {
const {
// these props are invalid for an `a` tag
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
replace,
to,
...rest
} = props;
return <a
{...rest}
onClick={onLinkClick}
style={props.style}
className={props.className}
ref={ref}
href={to}
>
{props.children}
</a>
}),
onLinkClick,
graphql: jest.fn(),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn(),
}
})

View File

@@ -0,0 +1,4 @@
import './gatsby';
import './gatsby-image';
import './disqus-react';
import './gatsby-plugin-google-analytics'

View File

@@ -0,0 +1 @@
module.exports = () => null;

View File

@@ -1,2 +1,4 @@
import "jest-dom/extend-expect"
import "@testing-library/react/cleanup-after-each"
import React from "react"
import "@testing-library/jest-dom/extend-expect"
import 'jest-axe/extend-expect';
import '../../__mocks__/modules';

View File

@@ -10,6 +10,6 @@ location /unicorns {
return 302 /about;
}
location /posts {
location = /posts/ {
return 301 /;
}

View File

@@ -0,0 +1,403 @@
Attribution-NonCommercial-NoDerivatives 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial-NoDerivatives 4.0
International Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-NoDerivatives 4.0 International Public
License ("Public License"). To the extent this Public License may be
interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the
Licensor receives from making the Licensed Material available under
these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
c. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
d. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
e. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
f. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
g. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
h. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
i. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce and reproduce, but not Share, Adapted Material
for NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material, You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
For the avoidance of doubt, You do not have permission under
this Public License to Share Adapted Material.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only and provided You do not Share Adapted Material;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,9 @@
These are the official emotes for Unicorn Utterances
<img alt="The 'happy' emote" src="./happy.png" width="75"/> <img alt="The 'hello' emote" src="./hello.png" width="75"/> <img alt="The 'mad' emote" src="./mad.png" width="75"/> <img alt="The 'neutral' emote" src="./neutral.png" width="75"/> <img alt="The 'proud' emote" src="./proud.png" width="75"/> <img alt="The 'sad' emote" src="./sad.png" width="75"/> <img alt="The 'scared' emote" src="./scared.png" width="75"/> <img alt="The 'tired' emote" src="./tired.png" width="75"/>
If being used in code, they can be randomized by using the individual parts of the emotes in [`emote-parts` folder](../emote-parts)
They're covered under [the CC by NC ND license](https://creativecommons.org/licenses/by-nc-nd/4.0/)
![The CC by NC ND license logo](https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -0,0 +1,37 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M369.219 175.953L289.022 38.375V218.313C229.963 258.825 182.478 314.984 152.545 380.812H77.9253L100.722 427.759L100.983 428.234C103.058 432.007 105.289 436.236 107.774 440.948L107.9 441.187C112.58 450.06 118.043 460.418 124.131 470.683C119.668 494.44 117.333 518.947 117.333 544C117.333 761.968 294.031 938.667 512 938.667C728.437 938.667 904.181 764.443 906.64 548.592C910.549 544.128 913.771 539.355 916.405 534.459C926.235 516.161 927.723 495.382 928.453 485.213L928.517 484.357C928.656 482.381 928.757 481.051 928.859 480.021L931.995 448.935L893.936 444.178C891.285 434.017 888.24 424.016 884.821 414.192L929.024 409.772V380.812C929.024 342.839 919.429 312.07 894.533 293.05C871.728 275.63 843.115 274.146 822.357 274.146H799.995C727.989 197.328 625.6 149.333 512 149.333C461.647 149.333 413.497 158.763 369.219 175.953Z" fill="#127DB3"/>
<path d="M310.417 662.48V515.49L178.286 634.405L184.916 642.299C209.128 671.125 234.68 680.773 257.217 680.773C279.174 680.773 296.707 671.621 305.667 665.648L310.417 662.48Z" fill="#82C3D9"/>
<path d="M310.417 662.48V515.49L178.286 634.405L184.916 642.299C209.128 671.125 234.68 680.773 257.217 680.773C279.174 680.773 296.707 671.621 305.667 665.648L310.417 662.48Z" fill="url(#paint0_radial)"/>
<path d="M310.356 402.146V117.333L417.791 301.638C356.982 316.594 316.545 356.937 311.007 402.146H310.356Z" fill="url(#paint1_linear)"/>
<path d="M662.283 582.613C644.96 601.867 626.075 615.797 606.709 625.413L706.096 795.771L704.373 800.48C687.163 847.525 652.688 876.053 616.096 892.661C579.691 909.184 540.763 914.144 513.022 914.144C388.187 914.144 328.663 841.717 311.218 801.013L310.355 799.003V515.551C284.947 527.406 262.556 532.361 242.633 531.774C218.487 531.063 198.921 522.237 182.94 509.394C167.139 496.694 154.9 480.124 144.903 463.821C138.132 452.778 132.049 441.244 126.644 430.996C124.181 426.325 121.858 421.921 119.676 417.953L112 402.146H311.007C318.017 344.914 380.958 295.479 470.355 295.479H822.357C842.971 295.479 864.96 297.305 881.584 310.003C898.896 323.228 907.691 345.923 907.691 380.812V390.466L708.928 410.342C700.656 411.169 694.357 418.13 694.357 426.444C694.357 434.605 700.432 441.489 708.528 442.501L908.677 467.52L907.637 477.882C907.493 479.274 907.376 480.937 907.237 482.816C906.485 493.182 905.259 510.126 897.611 524.358C892.837 533.244 885.531 541.365 874.507 546.901C863.616 552.379 849.808 554.971 832.507 554.133C822.795 553.664 812.379 552.709 801.909 551.755C797.056 551.307 792.192 550.864 787.376 550.464C771.893 549.184 756.379 548.315 741.275 549.115C711.2 550.704 683.707 558.811 662.283 582.613Z" fill="#F0FBFF"/>
<path d="M662.283 582.613C644.96 601.867 626.075 615.797 606.709 625.413L706.096 795.771L704.373 800.48C687.163 847.525 652.688 876.053 616.096 892.661C579.691 909.184 540.763 914.144 513.022 914.144C388.187 914.144 328.663 841.717 311.218 801.013L310.355 799.003V515.551C284.947 527.406 262.556 532.361 242.633 531.774C218.487 531.063 198.921 522.237 182.94 509.394C167.139 496.694 154.9 480.124 144.903 463.821C138.132 452.778 132.049 441.244 126.644 430.996C124.181 426.325 121.858 421.921 119.676 417.953L112 402.146H311.007C318.017 344.914 380.958 295.479 470.355 295.479H822.357C842.971 295.479 864.96 297.305 881.584 310.003C898.896 323.228 907.691 345.923 907.691 380.812V390.466L708.928 410.342C700.656 411.169 694.357 418.13 694.357 426.444C694.357 434.605 700.432 441.489 708.528 442.501L908.677 467.52L907.637 477.882C907.493 479.274 907.376 480.937 907.237 482.816C906.485 493.182 905.259 510.126 897.611 524.358C892.837 533.244 885.531 541.365 874.507 546.901C863.616 552.379 849.808 554.971 832.507 554.133C822.795 553.664 812.379 552.709 801.909 551.755C797.056 551.307 792.192 550.864 787.376 550.464C771.893 549.184 756.379 548.315 741.275 549.115C711.2 550.704 683.707 558.811 662.283 582.613Z" fill="url(#paint2_linear)"/>
<path d="M683.691 651.669V703.883C683.691 716.437 678.501 727.184 670.501 734.757L684.997 759.605C652.448 785.259 597.125 802.144 534.357 802.144C434.208 802.144 353.022 759.163 353.022 706.144C353.022 653.125 434.208 610.144 534.357 610.144C596.293 610.144 650.976 626.587 683.691 651.669Z" fill="url(#paint3_radial)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M508.657 705.877C536.773 689.392 536.773 648.741 508.657 632.256L449.266 597.445C420.822 580.768 385.022 601.28 385.022 634.251V703.883C385.022 736.853 420.823 757.365 449.266 740.693L508.657 705.877ZM560.053 705.877C531.935 689.392 531.935 648.741 560.053 632.256L619.445 597.445C647.888 580.768 683.691 601.28 683.691 634.251V703.883C683.691 736.853 647.888 757.365 619.445 740.693L560.053 705.877Z" fill="#153E67"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M508.657 705.877C536.773 689.392 536.773 648.741 508.657 632.256L449.266 597.445C420.822 580.768 385.022 601.28 385.022 634.251V703.883C385.022 736.853 420.823 757.365 449.266 740.693L508.657 705.877ZM560.053 705.877C531.935 689.392 531.935 648.741 560.053 632.256L619.445 597.445C647.888 580.768 683.691 601.28 683.691 634.251V703.883C683.691 736.853 647.888 757.365 619.445 740.693L560.053 705.877Z" fill="url(#paint4_radial)"/>
<path d="M534.357 621.856C504.9 621.856 481.022 645.733 481.022 675.189C481.022 704.645 504.9 728.523 534.357 728.523C563.808 728.523 587.691 704.645 587.691 675.189C587.691 645.733 563.808 621.856 534.357 621.856Z" fill="#266999"/>
<path d="M510 393L558 436.577L510 484" stroke="#2A2933" stroke-width="22"/>
<path d="M558 538C572.912 538 585 525.914 585 511.006L558 484L531 511.006C531 525.914 543.088 538 558 538Z" fill="#3669EC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M584.743 510.749C582.92 523.886 571.641 534 558 534C544.358 534 533.08 523.886 531.257 510.749L531 511.006C531 525.914 543.088 538 558 538C572.912 538 585 525.914 585 511.006L584.743 510.749Z" fill="#085099" fill-opacity="0.9"/>
<defs>
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(177.022 634.144) rotate(-41.7428) scale(232.306 237.356)">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#085099" stop-opacity="0.9"/>
</radialGradient>
<linearGradient id="paint1_linear" x1="363.689" y1="332.812" x2="310.355" y2="119.479" gradientUnits="userSpaceOnUse">
<stop stop-color="#E68A99"/>
<stop offset="1" stop-color="#FFC2E9"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="510.34" y1="295.479" x2="510.34" y2="914.144" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#E4F4FF"/>
</linearGradient>
<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(534.357 711.477) rotate(90) scale(58.6667 175.411)">
<stop stop-color="#266999" stop-opacity="0.2"/>
<stop offset="1" stop-color="#153E67" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint4_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(534.357 740.811) rotate(-90) scale(130.667)">
<stop stop-color="#262833"/>
<stop offset="1" stop-color="#262833" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,41 @@
<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M69.2285 32.9911L54.1916 7.19531V40.9337C43.118 48.5297 34.2146 59.0595 28.6022 71.4023H14.611L18.8853 80.2048L18.9343 80.2939C19.3234 81.0013 19.7417 81.7942 20.2077 82.6777L20.2313 82.7226C21.1087 84.3862 22.133 86.3283 23.2745 88.2531C22.4377 92.7075 22 97.3026 22 102C22 142.869 55.1309 176 96 176C136.582 176 169.534 143.333 169.995 102.861C170.728 102.024 171.332 101.129 171.826 100.211C173.669 96.7802 173.948 92.8842 174.085 90.9775L174.097 90.817C174.123 90.4465 174.142 90.197 174.161 90.004L174.749 84.1753L167.613 83.2833C167.116 81.3782 166.545 79.503 165.904 77.661L174.192 76.8323V71.4023C174.192 64.2823 172.393 58.5131 167.725 54.9469C163.449 51.6807 158.084 51.4023 154.192 51.4023H149.999C136.498 36.999 117.3 28 96 28C86.5589 28 77.5307 29.768 69.2285 32.9911Z" fill="#127DB3"/>
<path d="M58.2032 124.215V96.6543L33.4286 118.951L34.6718 120.431C39.2115 125.836 44.0025 127.645 48.2282 127.645C52.3451 127.645 55.6326 125.929 57.3126 124.809L58.2032 124.215Z" fill="#82C3D9"/>
<path d="M58.2032 124.215V96.6543L33.4286 118.951L34.6718 120.431C39.2115 125.836 44.0025 127.645 48.2282 127.645C52.3451 127.645 55.6326 125.929 57.3126 124.809L58.2032 124.215Z" fill="url(#paint0_radial)"/>
<path d="M58.1917 75.4023V22L78.3358 56.5571C66.9341 59.3613 59.3521 66.9257 58.3139 75.4023H58.1917Z" fill="url(#paint1_linear)"/>
<path d="M124.178 109.24C120.93 112.85 117.389 115.462 113.758 117.265L132.393 149.207L132.07 150.09C128.843 158.911 122.379 164.26 115.518 167.374C108.692 170.472 101.393 171.402 96.1916 171.402C72.7851 171.402 61.6243 157.822 58.3533 150.19L58.1916 149.813V96.6659C53.4275 98.8886 49.2293 99.8177 45.4937 99.7077C40.9663 99.5744 37.2976 97.9194 34.3012 95.5113C31.3385 93.1301 29.0437 90.0233 27.1694 86.9665C25.8998 84.8959 24.7592 82.7332 23.7458 80.8118C23.2839 79.936 22.8484 79.1102 22.4392 78.3662L21 75.4023H58.3139C59.6282 64.6714 71.4296 55.4023 88.1916 55.4023H154.192C158.057 55.4023 162.18 55.7447 165.297 58.1255C168.543 60.6052 170.192 64.8605 170.192 71.4023V73.2123L132.924 76.9391C131.373 77.0942 130.192 78.3994 130.192 79.9582C130.192 81.4885 131.331 82.7792 132.849 82.969L170.377 87.66L170.182 89.6029C170.155 89.8638 170.133 90.1756 170.107 90.528C169.966 92.4716 169.736 95.6487 168.302 98.3172C167.407 99.9833 166.037 101.506 163.97 102.544C161.928 103.571 159.339 104.057 156.095 103.9C154.274 103.812 152.321 103.633 150.358 103.454C149.448 103.37 148.536 103.287 147.633 103.212C144.73 102.972 141.821 102.809 138.989 102.959C133.35 103.257 128.195 104.777 124.178 109.24Z" fill="#F0FBFF"/>
<path d="M124.178 109.24C120.93 112.85 117.389 115.462 113.758 117.265L132.393 149.207L132.07 150.09C128.843 158.911 122.379 164.26 115.518 167.374C108.692 170.472 101.393 171.402 96.1916 171.402C72.7851 171.402 61.6243 157.822 58.3533 150.19L58.1916 149.813V96.6659C53.4275 98.8886 49.2293 99.8177 45.4937 99.7077C40.9663 99.5744 37.2976 97.9194 34.3012 95.5113C31.3385 93.1301 29.0437 90.0233 27.1694 86.9665C25.8998 84.8959 24.7592 82.7332 23.7458 80.8118C23.2839 79.936 22.8484 79.1102 22.4392 78.3662L21 75.4023H58.3139C59.6282 64.6714 71.4296 55.4023 88.1916 55.4023H154.192C158.057 55.4023 162.18 55.7447 165.297 58.1255C168.543 60.6052 170.192 64.8605 170.192 71.4023V73.2123L132.924 76.9391C131.373 77.0942 130.192 78.3994 130.192 79.9582C130.192 81.4885 131.331 82.7792 132.849 82.969L170.377 87.66L170.182 89.6029C170.155 89.8638 170.133 90.1756 170.107 90.528C169.966 92.4716 169.736 95.6487 168.302 98.3172C167.407 99.9833 166.037 101.506 163.97 102.544C161.928 103.571 159.339 104.057 156.095 103.9C154.274 103.812 152.321 103.633 150.358 103.454C149.448 103.37 148.536 103.287 147.633 103.212C144.73 102.972 141.821 102.809 138.989 102.959C133.35 103.257 128.195 104.777 124.178 109.24Z" fill="url(#paint2_linear)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M96.3648 78.1635C97.5781 77.661 98.8784 77.4023 100.192 77.4023C101.505 77.4023 102.805 77.661 104.018 78.1635C105.232 78.6661 106.334 79.4027 107.263 80.3313C108.191 81.2599 108.928 82.3623 109.43 83.5755C109.933 84.7888 110.192 86.0891 110.192 87.4023H106.192C106.192 86.6144 106.036 85.8342 105.735 85.1062C105.433 84.3783 104.991 83.7169 104.434 83.1597C103.877 82.6026 103.216 82.1606 102.488 81.8591C101.76 81.5575 100.98 81.4023 100.192 81.4023C99.4037 81.4023 98.6235 81.5575 97.8955 81.8591C97.1676 82.1606 96.5062 82.6026 95.949 83.1597C95.3919 83.7169 94.9499 84.3783 94.6484 85.1062C94.3468 85.8342 94.1917 86.6144 94.1917 87.4023H90.1917C90.1917 86.0891 90.4503 84.7888 90.9529 83.5755C91.4554 82.3623 92.192 81.2599 93.1206 80.3313C94.0492 79.4027 95.1516 78.6661 96.3648 78.1635Z" fill="#2A2933"/>
<path d="M128.192 122.188V131.978C128.192 134.332 127.219 136.347 125.719 137.767L128.437 142.426C122.334 147.236 111.961 150.402 100.192 150.402C81.414 150.402 66.1917 142.343 66.1917 132.402C66.1917 122.461 81.414 114.402 100.192 114.402C111.805 114.402 122.058 117.485 128.192 122.188Z" fill="url(#paint3_radial)"/>
<path d="M95.3731 132.352C100.645 129.261 100.645 121.639 95.3731 118.548L84.2374 112.021C78.9042 108.894 72.1916 112.74 72.1917 118.922V131.978C72.1917 138.16 78.9043 142.006 84.2374 138.88L95.3731 132.352Z" fill="#153E67"/>
<path d="M95.3731 132.352C100.645 129.261 100.645 121.639 95.3731 118.548L84.2374 112.021C78.9042 108.894 72.1916 112.74 72.1917 118.922V131.978C72.1917 138.16 78.9043 142.006 84.2374 138.88L95.3731 132.352Z" fill="url(#paint4_radial)"/>
<path d="M105.01 132.352C99.7378 129.261 99.7378 121.639 105.01 118.548L116.146 112.021C121.479 108.894 128.192 112.74 128.192 118.922V131.978C128.192 138.16 121.479 142.006 116.146 138.88L105.01 132.352Z" fill="#153E67"/>
<path d="M105.01 132.352C99.7378 129.261 99.7378 121.639 105.01 118.548L116.146 112.021C121.479 108.894 128.192 112.74 128.192 118.922V131.978C128.192 138.16 121.479 142.006 116.146 138.88L105.01 132.352Z" fill="url(#paint5_radial)"/>
<path d="M100.192 116.598C94.6688 116.598 90.1917 121.075 90.1917 126.598C90.1917 132.121 94.6688 136.598 100.192 136.598C105.714 136.598 110.192 132.121 110.192 126.598C110.192 121.075 105.714 116.598 100.192 116.598Z" fill="#266999"/>
<defs>
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(33.1917 118.902) rotate(-41.7428) scale(43.5574 44.5042)">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#085099" stop-opacity="0.9"/>
</radialGradient>
<linearGradient id="paint1_linear" x1="68.1917" y1="62.4023" x2="58.1916" y2="22.4023" gradientUnits="userSpaceOnUse">
<stop stop-color="#E68A99"/>
<stop offset="1" stop-color="#FFC2E9"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="95.6887" y1="55.4023" x2="95.6887" y2="171.402" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#E4F4FF"/>
</linearGradient>
<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(100.192 133.402) rotate(90) scale(11 32.8896)">
<stop stop-color="#266999" stop-opacity="0.2"/>
<stop offset="1" stop-color="#153E67" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint4_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(100.192 138.902) rotate(-90) scale(24.5)">
<stop stop-color="#262833"/>
<stop offset="1" stop-color="#262833" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint5_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(100.192 138.902) rotate(-90) scale(24.5)">
<stop stop-color="#262833"/>
<stop offset="1" stop-color="#262833" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,34 @@
<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M58.2032 124.215V96.6543L33.4286 118.951L34.6718 120.431C39.2115 125.836 44.0025 127.645 48.2282 127.645C52.3451 127.645 55.6326 125.929 57.3126 124.809L58.2032 124.215Z" fill="#82C3D9"/>
<path d="M58.2032 124.215V96.6543L33.4286 118.951L34.6718 120.431C39.2115 125.836 44.0025 127.645 48.2282 127.645C52.3451 127.645 55.6326 125.929 57.3126 124.809L58.2032 124.215Z" fill="url(#paint0_radial)"/>
<path d="M58.1917 75.4023V22L78.3358 56.5571C66.9341 59.3613 59.3521 66.9257 58.3139 75.4023H58.1917Z" fill="url(#paint1_linear)"/>
<path d="M124.178 109.24C120.93 112.85 117.389 115.462 113.758 117.265L132.393 149.207L132.07 150.09C128.843 158.911 122.379 164.26 115.518 167.374C108.692 170.472 101.393 171.402 96.1916 171.402C72.7851 171.402 61.6243 157.822 58.3533 150.19L58.1916 149.813V96.6659C53.4275 98.8886 49.2293 99.8177 45.4937 99.7077C40.9663 99.5744 37.2976 97.9194 34.3012 95.5113C31.3385 93.1301 29.0437 90.0233 27.1694 86.9665C25.8998 84.8959 24.7592 82.7332 23.7458 80.8118C23.2839 79.936 22.8484 79.1102 22.4392 78.3662L21 75.4023H58.3139C59.6282 64.6714 71.4296 55.4023 88.1916 55.4023H154.192C158.057 55.4023 162.18 55.7447 165.297 58.1255C168.543 60.6052 170.192 64.8605 170.192 71.4023V73.2123L132.924 76.9391C131.373 77.0942 130.192 78.3994 130.192 79.9582C130.192 81.4885 131.331 82.7792 132.849 82.969L170.377 87.66L170.182 89.6029C170.155 89.8638 170.133 90.1756 170.107 90.528C169.966 92.4716 169.736 95.6487 168.302 98.3172C167.407 99.9833 166.037 101.506 163.97 102.544C161.928 103.571 159.339 104.057 156.095 103.9C154.274 103.812 152.321 103.633 150.358 103.454C149.448 103.37 148.536 103.287 147.633 103.212C144.73 102.972 141.821 102.809 138.989 102.959C133.35 103.257 128.195 104.777 124.178 109.24Z" fill="#F0FBFF"/>
<path d="M124.178 109.24C120.93 112.85 117.389 115.462 113.758 117.265L132.393 149.207L132.07 150.09C128.843 158.911 122.379 164.26 115.518 167.374C108.692 170.472 101.393 171.402 96.1916 171.402C72.7851 171.402 61.6243 157.822 58.3533 150.19L58.1916 149.813V96.6659C53.4275 98.8886 49.2293 99.8177 45.4937 99.7077C40.9663 99.5744 37.2976 97.9194 34.3012 95.5113C31.3385 93.1301 29.0437 90.0233 27.1694 86.9665C25.8998 84.8959 24.7592 82.7332 23.7458 80.8118C23.2839 79.936 22.8484 79.1102 22.4392 78.3662L21 75.4023H58.3139C59.6282 64.6714 71.4296 55.4023 88.1916 55.4023H154.192C158.057 55.4023 162.18 55.7447 165.297 58.1255C168.543 60.6052 170.192 64.8605 170.192 71.4023V73.2123L132.924 76.9391C131.373 77.0942 130.192 78.3994 130.192 79.9582C130.192 81.4885 131.331 82.7792 132.849 82.969L170.377 87.66L170.182 89.6029C170.155 89.8638 170.133 90.1756 170.107 90.528C169.966 92.4716 169.736 95.6487 168.302 98.3172C167.407 99.9833 166.037 101.506 163.97 102.544C161.928 103.571 159.339 104.057 156.095 103.9C154.274 103.812 152.321 103.633 150.358 103.454C149.448 103.37 148.536 103.287 147.633 103.212C144.73 102.972 141.821 102.809 138.989 102.959C133.35 103.257 128.195 104.777 124.178 109.24Z" fill="url(#paint2_linear)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M96.3648 78.1635C97.5781 77.661 98.8784 77.4023 100.192 77.4023C101.505 77.4023 102.805 77.661 104.018 78.1635C105.232 78.6661 106.334 79.4027 107.263 80.3313C108.191 81.2599 108.928 82.3623 109.43 83.5755C109.933 84.7888 110.192 86.0891 110.192 87.4023H106.192C106.192 86.6144 106.036 85.8342 105.735 85.1062C105.433 84.3783 104.991 83.7169 104.434 83.1597C103.877 82.6026 103.216 82.1606 102.488 81.8591C101.76 81.5575 100.98 81.4023 100.192 81.4023C99.4037 81.4023 98.6235 81.5575 97.8955 81.8591C97.1676 82.1606 96.5062 82.6026 95.949 83.1597C95.3919 83.7169 94.9499 84.3783 94.6484 85.1062C94.3468 85.8342 94.1917 86.6144 94.1917 87.4023H90.1917C90.1917 86.0891 90.4503 84.7888 90.9529 83.5755C91.4554 82.3623 92.192 81.2599 93.1206 80.3313C94.0492 79.4027 95.1516 78.6661 96.3648 78.1635Z" fill="#2A2933"/>
<path d="M128.192 122.188V131.978C128.192 134.332 127.219 136.347 125.719 137.767L128.437 142.426C122.334 147.236 111.961 150.402 100.192 150.402C81.414 150.402 66.1917 142.343 66.1917 132.402C66.1917 122.461 81.414 114.402 100.192 114.402C111.805 114.402 122.058 117.485 128.192 122.188Z" fill="url(#paint3_radial)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.3731 132.352C100.645 129.261 100.645 121.639 95.3731 118.548L84.2374 112.021C78.9042 108.894 72.1916 112.74 72.1917 118.922V131.978C72.1917 138.16 78.9043 142.006 84.2374 138.88L95.3731 132.352ZM105.01 132.352C99.7378 129.261 99.7378 121.639 105.01 118.548L116.146 112.021C121.479 108.894 128.192 112.74 128.192 118.922V131.978C128.192 138.16 121.479 142.006 116.146 138.88L105.01 132.352Z" fill="#153E67"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.3731 132.352C100.645 129.261 100.645 121.639 95.3731 118.548L84.2374 112.021C78.9042 108.894 72.1916 112.74 72.1917 118.922V131.978C72.1917 138.16 78.9043 142.006 84.2374 138.88L95.3731 132.352ZM105.01 132.352C99.7378 129.261 99.7378 121.639 105.01 118.548L116.146 112.021C121.479 108.894 128.192 112.74 128.192 118.922V131.978C128.192 138.16 121.479 142.006 116.146 138.88L105.01 132.352Z" fill="url(#paint4_radial)"/>
<path d="M100.192 116.598C94.6688 116.598 90.1917 121.075 90.1917 126.598C90.1917 132.121 94.6688 136.598 100.192 136.598C105.714 136.598 110.192 132.121 110.192 126.598C110.192 121.075 105.714 116.598 100.192 116.598Z" fill="#266999"/>
<defs>
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(33.1917 118.902) rotate(-41.7428) scale(43.5574 44.5042)">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#085099" stop-opacity="0.9"/>
</radialGradient>
<linearGradient id="paint1_linear" x1="68.1917" y1="62.4023" x2="58.1916" y2="22.4023" gradientUnits="userSpaceOnUse">
<stop stop-color="#E68A99"/>
<stop offset="1" stop-color="#FFC2E9"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="95.6887" y1="55.4023" x2="95.6887" y2="171.402" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#E4F4FF"/>
</linearGradient>
<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(100.192 133.402) rotate(90) scale(11 32.8896)">
<stop stop-color="#266999" stop-opacity="0.2"/>
<stop offset="1" stop-color="#153E67" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint4_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(100.192 138.902) rotate(-90) scale(24.5)">
<stop stop-color="#262833"/>
<stop offset="1" stop-color="#262833" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,4 @@
<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M78.4691 55.7097L58.2361 21V74.4023H21L22.4472 77.2968C22.68 77.7624 22.9221 78.2549 23.1747 78.7689C25.5391 83.5796 28.8319 90.2795 34.1669 94.5183C37.1959 96.9249 40.9089 98.5748 45.4684 98.7077C49.2344 98.8176 53.46 97.8895 58.2361 95.6647V148.813L58.3978 149.19C61.6687 156.822 72.8295 170.402 96.2361 170.402C101.437 170.402 108.736 169.472 115.563 166.374C122.424 163.26 128.887 157.911 132.114 149.09L132.439 148.203L125.736 136.793C127.251 135.371 128.236 133.346 128.236 130.978V117.922C128.236 114.054 125.608 111.101 122.334 110.197C122.973 109.58 123.603 108.928 124.223 108.24C128.239 103.777 133.395 102.257 139.034 101.959C141.866 101.809 144.774 101.972 147.678 102.212C148.579 102.287 149.49 102.37 150.399 102.453L150.401 102.453L150.402 102.454C152.366 102.633 154.318 102.812 156.139 102.9C159.383 103.057 161.972 102.571 164.015 101.544C166.082 100.506 167.451 98.9833 168.347 97.3172C169.781 94.6487 170.011 91.4716 170.152 89.528C170.177 89.1756 170.2 88.8638 170.226 88.6029L170.422 86.66L132.894 81.969C131.376 81.7792 130.236 80.4885 130.236 78.9582C130.236 77.3994 131.417 76.0942 132.968 75.9391L170.236 72.2123V70.4023C170.236 63.8605 168.587 59.6052 165.341 57.1255C162.225 54.7447 158.102 54.4023 154.236 54.4023H88.2361C84.6591 54.4023 81.4808 54.8397 78.4691 55.7097ZM91.876 120.109L82.2589 114.471C79.5924 112.908 76.2361 114.831 76.2361 117.922V130.978C76.2361 134.069 79.5924 135.992 82.259 134.429L90.9558 129.33C90.4916 128.177 90.2361 126.918 90.2361 125.598C90.2361 123.571 90.8393 121.685 91.876 120.109ZM109.516 129.331L118.213 134.429C120.88 135.992 124.236 134.069 124.236 130.978V117.922C124.236 114.831 120.88 112.908 118.213 114.471L108.596 120.109C109.633 121.685 110.236 123.571 110.236 125.598C110.236 126.918 109.981 128.177 109.516 129.331ZM100.236 76.4023C98.9229 76.4023 97.6225 76.661 96.4092 77.1635C95.196 77.6661 94.0936 78.4027 93.165 79.3313C92.2364 80.2599 91.4998 81.3623 90.9973 82.5755C90.4947 83.7888 90.2361 85.0891 90.2361 86.4023H94.2361C94.2361 85.6144 94.3913 84.8342 94.6928 84.1062C94.9943 83.3783 95.4363 82.7169 95.9934 82.1597C96.5506 81.6026 97.212 81.1606 97.94 80.8591C98.6679 80.5575 99.4482 80.4023 100.236 80.4023C101.024 80.4023 101.804 80.5575 102.532 80.8591C103.26 81.1606 103.922 81.6026 104.479 82.1597C105.036 82.7169 105.478 83.3783 105.779 84.1062C106.081 84.8342 106.236 85.6144 106.236 86.4023H110.236C110.236 85.0891 109.977 83.7888 109.475 82.5755C108.972 81.3623 108.236 80.2599 107.307 79.3313C106.379 78.4027 105.276 77.6661 104.063 77.1635C102.85 76.661 101.549 76.4023 100.236 76.4023ZM94.2361 125.598C94.2361 122.285 96.9224 119.598 100.236 119.598C103.55 119.598 106.236 122.285 106.236 125.598C106.236 128.912 103.55 131.598 100.236 131.598C96.9224 131.598 94.2361 128.912 94.2361 125.598Z" fill="black"/>
<path d="M54.2361 129.473V101.912L29.4615 124.209L30.7047 125.689C35.2445 131.093 40.0355 132.902 44.2611 132.902C48.378 132.902 51.6655 131.186 53.3455 130.066L54.2361 129.473Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,5 @@
<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100.236 76.3978C98.9229 76.3978 97.6225 76.6565 96.4092 77.159C95.196 77.6616 94.0936 78.3982 93.165 79.3268C92.2364 80.2553 91.4998 81.3577 90.9973 82.571C90.4947 83.7842 90.2361 85.0846 90.2361 86.3978H94.2361C94.2361 85.6099 94.3913 84.8297 94.6928 84.1017C94.9943 83.3738 95.4363 82.7123 95.9934 82.1552C96.5506 81.598 97.212 81.1561 97.94 80.8545C98.6679 80.553 99.4482 80.3978 100.236 80.3978C101.024 80.3978 101.804 80.553 102.532 80.8545C103.26 81.1561 103.922 81.598 104.479 82.1552C105.036 82.7123 105.478 83.3738 105.779 84.1017C106.081 84.8297 106.236 85.6099 106.236 86.3978H110.236C110.236 85.0846 109.977 83.7842 109.475 82.571C108.972 81.3577 108.236 80.2553 107.307 79.3268C106.379 78.3982 105.276 77.6616 104.063 77.159C102.85 76.6565 101.549 76.3978 100.236 76.3978Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.2361 21L78.4881 55.7177C81.481 54.8715 84.7444 54.3978 88.2361 54.3978H154.236C158.102 54.3978 162.225 54.7402 165.341 57.121C168.587 59.6007 170.236 63.856 170.236 70.3978V72.2078L132.968 75.9346C131.417 76.0897 130.236 77.3949 130.236 78.9537C130.236 80.4839 131.376 81.7747 132.894 81.9645L170.422 86.6555L170.226 88.5984C170.2 88.8578 170.177 89.1675 170.152 89.5173L170.152 89.5204L170.152 89.5224L170.152 89.5235C170.011 91.4671 169.781 94.6442 168.347 97.3127C167.451 98.9788 166.082 100.501 164.015 101.54C161.972 102.566 159.383 103.053 156.139 102.895C154.318 102.807 152.366 102.629 150.402 102.449L150.399 102.449L150.396 102.449C149.489 102.366 148.578 102.282 147.678 102.208C144.774 101.967 141.866 101.805 139.034 101.954C133.395 102.252 128.239 103.773 124.223 108.236C123.603 108.924 122.973 109.576 122.335 110.193C125.608 111.096 128.236 114.05 128.236 117.918V130.973C128.236 133.327 127.263 135.342 125.764 136.762L132.437 148.202L132.114 149.085C128.887 157.906 122.424 163.255 115.563 166.369C108.736 169.467 101.437 170.398 96.2361 170.398C72.8295 170.398 61.6687 156.818 58.3978 149.186L58.2361 148.808V95.6602C53.46 97.885 49.2344 98.8131 45.4684 98.7032C40.9089 98.5703 37.1959 96.9204 34.1669 94.5138C28.8319 90.275 25.5391 83.5751 23.1747 78.7644C22.9221 78.2504 22.68 77.7579 22.4472 77.2923L21 74.3978H58.2361V21ZM122.272 138.715C120.33 139.234 118.165 139.033 116.19 137.875L107.305 132.667C105.496 134.475 102.997 135.594 100.236 135.594C97.4755 135.594 94.9762 134.475 93.1667 132.667L84.2818 137.875C78.9487 141.001 72.2361 137.155 72.2361 130.973V117.918C72.2361 111.736 78.9487 107.89 84.2818 111.016L85.4878 111.723L85.4909 111.718L85.5591 111.765L87.4233 112.858C95.1742 116.813 110.006 118.053 121.249 105.56C126.233 100.023 132.583 98.2893 138.823 97.9599C141.93 97.7959 145.053 97.9767 148.008 98.2214C148.992 98.3029 149.946 98.3903 150.876 98.4754L150.877 98.4755C152.79 98.6506 154.598 98.816 156.333 98.9002C159.089 99.0338 160.95 98.6033 162.219 97.9656C163.464 97.3398 164.269 96.4509 164.823 95.4193C165.7 93.787 165.932 91.924 166.082 90.1441L132.398 85.9336C128.878 85.4936 126.236 82.5012 126.236 78.9537C126.236 75.3399 128.974 72.314 132.57 71.9544L166.186 68.5929C165.912 63.8676 164.544 61.5456 162.913 60.2997C160.957 58.8054 158.08 58.3978 154.236 58.3978H88.2361C71.8844 58.3978 62.2361 69.7423 62.2361 78.3978H27.46C29.749 82.9852 32.5112 88.0894 36.6552 91.3819C39.1012 93.3253 42.0008 94.6004 45.585 94.7049C49.2011 94.8104 53.684 93.7286 59.2699 90.6467L62.2361 89.0102V147.976C65.178 154.441 75.0491 166.398 96.2361 166.398C101.035 166.398 107.736 165.528 113.91 162.727C119.811 160.048 125.157 155.646 128.024 148.576L122.272 138.715ZM109.516 129.326L118.213 134.424C120.88 135.987 124.236 134.064 124.236 130.973V117.918C124.236 114.827 120.88 112.904 118.213 114.467L108.596 120.104C109.633 121.68 110.236 123.567 110.236 125.594C110.236 126.913 109.981 128.173 109.516 129.326ZM90.2361 125.594C90.2361 126.97 90.514 128.281 91.0167 129.474C91.0061 129.449 90.9957 129.424 90.9853 129.399C90.9754 129.374 90.9655 129.35 90.9558 129.326L82.259 134.424C79.5924 135.987 76.2361 134.064 76.2361 130.973V117.918C76.2361 114.827 79.5924 112.904 82.2589 114.467L83.4327 115.155C84.0496 115.565 84.7222 115.96 85.4445 116.334L91.876 120.104C90.8393 121.68 90.2361 123.567 90.2361 125.594ZM62.2361 35.7957V66.7999C65.0683 62.8126 69.3081 59.3197 74.6435 57.0655L62.2361 35.7957ZM94.2361 125.594C94.2361 122.28 96.9224 119.594 100.236 119.594C103.55 119.594 106.236 122.28 106.236 125.594C106.236 128.907 103.55 131.594 100.236 131.594C96.9224 131.594 94.2361 128.907 94.2361 125.594Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.2361 101.907V129.468L53.3455 130.062C51.6655 131.182 48.378 132.898 44.2611 132.898C40.0355 132.898 35.2445 131.089 30.7047 125.684L29.4615 124.204L54.2361 101.907ZM35.0676 124.54C38.4548 127.961 41.6668 128.898 44.2611 128.898C46.7101 128.898 48.8196 128.061 50.2361 127.275V110.889L35.0676 124.54Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -5,7 +5,8 @@
published: '2019-07-11T22:12:03.284Z',
authors: ['crutchcorn'],
tags: ['angular', 'templates'],
attached: []
attached: [],
license: 'cc-by-nc-sa-4'
}
---
@@ -54,6 +55,7 @@ While Angular templates come in many shapes and sizes, a simple but common use f
```
<iframe src="https://stackblitz.com/edit/start-to-source-1-ng-template?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
In this example, we are creating a template and assigning it to a [template reference variable](https://blog.angulartraining.com/tutorial-the-magic-of-template-reference-variables-3183f0a0d9d1). _This template reference variable makes `falseTemp` a valid variable to use as a value for other inputs in the same template._ It then handles that variable similarly to how a variable from the component logic is handled when referenced from the template.
We are then adding the [`ngIf`](https://angular.io/api/common/NgIf) structural directive to the paragraph tag to render content to the screen conditionally.
@@ -79,6 +81,7 @@ But there's a ~~simpler~~ ~~much more complex~~ another way show the same templa
```
<iframe src="https://stackblitz.com/edit/start-to-source-2-conditional-render?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
> While this is not how the `ngIf` structural template works internally, this is a good introduction to the `ngTemplateOutlet` directive, which adds functionality to the `ng-template` tag.
>
> If you're curious to how Angular's `ngIf` works, read on dear reader.
@@ -127,6 +130,7 @@ Here, you can see that `let-templateVariableName="contextKeyName"` is the syntax
Now let's see it in action!
<iframe src="https://stackblitz.com/edit/start-to-source-3-context?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
As a quick note, _I only named these template input variables differently from the context value key to make it clear that you may do so_. `let-personName="personName"` is not only valid, but it also can make the code's intentions clearer to other developers.
# View References — `ViewChild`/`ContentChild` {#view-references}
@@ -154,6 +158,7 @@ export class AppComponent {
```
<iframe src="https://stackblitz.com/edit/start-to-source-4-viewchild?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
> While this example is effectively not-much-more than an alternative API to `ngTemplateOutlet`, it serves as a basis for introducing into further concepts.
_`ViewChild` is a "property decorator" utility for Angular that searches the component tree to find what you pass it as a query._ In the example above, when we pass the string `'templName'`, we are looking for something in the tree that is marked with the template variable `helloMsg`. In this case, it's an `ng-template`, which is then stored to the `helloMessageTemplate` property when this is found. Because it is a reference to a template, we are typing it as `TemplateRef<any>` to have TypeScript understand the typings whenever it sees this variable.
@@ -184,6 +189,7 @@ console.log(this.myComponent.inputHere); // This will print `50`
It would give you the property value on the instance of that component. Angular by default does a pretty good job at figuring out what it is that you wanted to get a reference of and returning the "correct" object for that thing.
<iframe src="https://stackblitz.com/edit/start-to-source-5-view-not-template?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Despite the examples thus far having only used a string as the query for `ViewChild`, you're also able to use the ComponentClass to query for a component with that component type.
```typescript
@@ -217,6 +223,7 @@ console.log(myComponent.nativeElement.dataset.getAttribute('data-unrelatedAttr')
```
<iframe src="https://stackblitz.com/edit/start-to-source-6-read-prop?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
`ViewChild` isn't an only child, though (get it?). There are other APIs similar to it that allow you to get references to other items in your templates from your component logic.
## `ViewChildren`: More references then your nerdy pop culture friend {#viewchildren}
@@ -239,6 +246,7 @@ export class AppComponent {
```
<iframe src="https://stackblitz.com/edit/start-to-source-7-viewchildren?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Would give you a list of all components with that base class. You're also able to use the `{read: ElementRef}` property from the `ViewChild` property decorator to get a `QueryList<ElementRef>` (to be able to get a reference to the DOM [Elements](https://developer.mozilla.org/en-US/docs/Web/API/Element) themselves) instead of a query list of `MyComponentComponent` types.
### What is `QueryList` {#viewchildren-querylist}
@@ -269,6 +277,7 @@ this.myComponents.changes.subscribe(compsQueryList => {
```
<iframe src="https://stackblitz.com/edit/start-to-source-8-querylist?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
It might be a good idea to gain familiarity of doing this as the Angular docs give the following warning in the [`QueryList` docs](https://angular.io/api/core/QueryList#changes):
> NOTE: In the future this class will implement an Observable interface.
@@ -348,6 +357,7 @@ export class CardsList implements AfterViewInit {
Awesome, let's spin that up and… Oh.
<iframe src="https://stackblitz.com/edit/start-to-source-9-cardlist-broke?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
The cards are still grey. Let's open up our terminal and see if the `console.log`s ran.
They didn't.
@@ -363,6 +373,7 @@ If we change the `ViewChildren` line to read:
```
<iframe src="https://stackblitz.com/edit/start-to-source-10-cardlist-fixed?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
We'll see that the code now runs as expected. The cards are recolored, the `consoles.log`s ran, and the developers are happy.
### The Content Without the `ng` {#viewchildren-without-ng-content}
@@ -611,6 +622,7 @@ Straightforward enough example, lets see a more difficult example:
```
<iframe src="https://stackblitz.com/edit/start-to-source-11-broke-template-var?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
If you look at the output of this example, you'll notice that `testingMessage` isn't rendering. This is because template reference variables bind to the view that they're present in; and as a result are unable to be accessed from parent views.
[Like how CSS is applied to a dom when bound to a selector](#the-dom), template reference variables can be accessed within the view itself and child views, but not the parent views.
@@ -635,6 +647,7 @@ In order to fix this behavior, we'd need to move the second `ng-template` into t
```
<iframe src="https://stackblitz.com/edit/start-to-source-12-fixed-template-var?embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
# The Bane of All JavaScipt Developer: Timings {#timings}
## Understanding timings with `ViewChildren` {#viewchildren-timings}
@@ -721,6 +734,7 @@ export class AppComponent implements DoCheck, OnChanges, AfterViewInit {
```
<iframe src="https://stackblitz.com/edit/start-to-source-13-lifecycle-explain?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Looking at the console logs, you'll be left with the following messages in your console:
```diff
@@ -769,6 +783,7 @@ export class AppComponent {
```
<iframe src="https://stackblitz.com/edit/start-to-source-14-static?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Because this example does not have the `helloThereMsg` template within another view (outside of the host view), it is able to render without the errors we found when using `static: true`). Likewise, if you were to add an `OnInit` lifecycle method, you'd be able to get a reference to that template.
```typescript
@@ -782,6 +797,7 @@ While you might wonder "Why would you use `static: false` if you can get the acc
When taking the example with the `testingMessageCompVar` prop and changing the value to `true`, it will never render the other component since it will always stay `undefined`.
<iframe src="https://stackblitz.com/edit/start-to-source-15-static-first-check?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
# View Manipulation {#view-manipulation}
## View Limitations {#view-limitations}
@@ -823,6 +839,7 @@ export class AppComponent implements OnInit {
```
<iframe src="https://stackblitz.com/edit/start-to-source-16-createembeddedview?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
This example has a lot going on, so let's dissect it bit-by-bit.
Starting with some small recap:
@@ -893,6 +910,7 @@ ngOnInit() {
```
<iframe src="https://stackblitz.com/edit/start-to-source-17-see-viewcontainer-indexes?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
#### Context
Just as we can use `contextRouterOutlet`, you're able to pass context to a template when rendering it using `createEmbeddedView`. So, let's say that you wanted to have a counting component and want to pass a specific index to start counting from, you could pass a context, [with the same object structure we did before](#template-context), have:
@@ -935,6 +953,7 @@ To get around this, we can use the `ng-container` tag, which allows us to get a
<iframe src="https://stackblitz.com/edit/start-to-source-18-create-embedd-context?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
#### Move/Insert Template
But oh no! You'll see that the ordering is off. The simplest (and probably most obvious) solution would be to flip the order of the calls. After all, if they're based on index — moving the two calls to be in the opposite order would just fix the problem.
@@ -948,6 +967,7 @@ this.viewContainerRef.move(embeddRef1, newViewIndex); // This will move this vie
```
<iframe src="https://stackblitz.com/edit/start-to-source-19-move-template?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Angular provides many APIs to take an existing view and move it and modify it without having to create a new one and run change detection/etc again.
If you're wanting to try out a different API and feel that `createEmbeddedView` is a little too high-level for you (we need to go deeper), you can create a view from a template and then embed it yourself manually.
@@ -962,6 +982,7 @@ ngOnInit() {
```
<iframe src="https://stackblitz.com/edit/start-to-source-20-insert-template?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
[And in fact, this is how the `createEmbeddedView` works internally](https://github.com/angular/angular/blob/e1f6d1538784eb87f7497bef27e3c313184c2d30/packages/core/src/view/refs.ts#L174):
```typescript
@@ -1007,6 +1028,7 @@ export class AppComponent {}
```
<iframe src="https://stackblitz.com/edit/start-to-source-21-directive-template?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
You'll notice this code is almost exactly the same from some of our previous component code.
## Reference More Than View Containers {#directive-template-ref}
@@ -1038,6 +1060,7 @@ export class AppComponent {}
```
<iframe src="https://stackblitz.com/edit/start-to-source-22-directive-template-reference?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
## Input Shorthand {#directive-same-name-input}
With directives, we can even create an input with the same name, and just pass that input value directly to the template using a context:
@@ -1070,6 +1093,7 @@ export class AppComponent {}
> I want to make clear that this trick is present in all directives. If you name the input the same as the directive name, it will bind the value you're passing in to that directive name while also associating the directive with the component. No need for a separate input and directive name!
<iframe src="https://stackblitz.com/edit/start-to-source-23-directive-input-name?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Starting to look a bit more like the `ngTemplateOutlet`, no? Well, why not go even further! Let's lean into that!
With this syntax, we can add a second input, pass an object as the context to the template we want to render, and then a template reference variable, and be able to recreate Angular's `ngTemplateOutlet`'s API almost to-a-T:
@@ -1103,6 +1127,7 @@ export class AppComponent {}
```
<iframe src="https://stackblitz.com/edit/start-to-source-24-directive-outlet-alternative?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
The nice part is that not only does it look like the directive from its usage, [but it's also not entirely dissimilar to how Angular writes the component internally](https://github.com/angular/angular/blob/e1f6d1538784eb87f7497bef27e3c313184c2d30/packages/common/src/directives/ng_template_outlet.ts#L35):
```typescript
@@ -1165,6 +1190,7 @@ export class AppComponent {}
```
<iframe src="https://stackblitz.com/edit/start-to-source-25-structural-directive-intro?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
[Just as we previously used Angular's dependency injection (DI) system to get a reference to the `ViewContainerRef`](#embed-views), we're using DI to get a reference to the `TemplateRef` created by the `*` in the invocation of this directive and embedding a view.
Too much CS (computer science) speak? Me too, let's rephrase that. When you add the `*` to the start of the directive that's being attached to the element, you're essentially telling Angular to wrap that element in an `ng-template` and pass the directive to the newly created template.
@@ -1182,6 +1208,7 @@ The cool part about structural directives, though? Because they're simply direct
```
<iframe src="https://stackblitz.com/edit/start-to-source-26-structural-directive-manually-apply?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
It is for this reason that **only one structural directive can be applied to one element**. Otherwise, how would it know what order to wrap those directives in? What template should get what reference to what template?
### Building A Basic `*ngIf`
@@ -1225,6 +1252,7 @@ export class AppComponent {
```
<iframe src="https://stackblitz.com/edit/start-to-source-27-render-if-intro?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Super cool! Image we kept developing this structural directive out, but you noticed while running your test (which you should totally have 👀) that toggling the checkbox doesn't actually show anything! This is because it's running the check once on `ngOnInit` and not again when the input changes. So let's change that:
```typescript
@@ -1252,6 +1280,7 @@ export class RenderThisIfDirective {
```
<iframe src="https://stackblitz.com/edit/start-to-source-28-render-if-work-toggle-true?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
You'll notice that I removed the `OnInit` lifecycle and replaced it with an input `set`ter. We could have changed the lifecycle method to use `ngOnChanges` to listen for input changes, given that we only have one input, but as your directive adds more inputs and you want to maintain the local state, that logic can get more complex.
Running our tests again, we see that toggling it once now shows the embedded view, but toggling it again after that does not hide it again. With a simple update to the `update` method, we can fix that:
@@ -1267,6 +1296,7 @@ update(): void {
```
<iframe src="https://stackblitz.com/edit/start-to-source-29-render-if-fully-working?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Here, we're using the `clear` method on the parent view ref to remove the previous view when the value is false. Because our structural directive will contain a template only used for this directive, we can safely assume that `clear` will only remove templates created within this directive and not from an external source.
#### How Angular Built It {#angular-ngif-source}
@@ -1365,6 +1395,7 @@ export class AppComponent {}
````
<iframe src="https://stackblitz.com/edit/start-to-source-30-microsyntax?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
This might look familiar. We're using the `$implicit` value from the context within our structural directive! However, [if you review the section we introduced that concept in](#template-context), you'll notice that the syntax here is different but similar from a template variable that would be used to bind the context from an `ng-template` tag.
The semicolon is the primary differentiator between the two syntaxes in this particular example. The semicolon marks the end to the previous statement and the start of a new one (the first statement being a binding of the `makePiglatin` property in the directive, the second being a binding of the `$implicit` context value to the local template variable `msg`). This small demo already showcases part of why the microsyntax is so nice — it allows you to have a micro-language to define your APIs.
@@ -1400,6 +1431,7 @@ export class AppComponent {}
<iframe src="https://stackblitz.com/edit/start-to-source-31-structural-named-context?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Just as before, we would use semicolons to split the definitions, then bind the external (as in: from the directive) context value of `original` to the local (this template) variable of `ogMsg`.
@@ -1431,6 +1463,7 @@ And then call them with the following template:
```
<iframe src="https://stackblitz.com/edit/start-to-source-32-console-non-structural-directive?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
This can be super useful for both providing concise APIs as well as provide further functionalities to said directive simply. Structural directives offer similar, although it comes with its own syntax and limitations due to the microsyntax API.
```typescript
@@ -1470,6 +1503,7 @@ export class AppComponent { }
```
<iframe src="https://stackblitz.com/edit/start-to-source-33-pig-latin-microsyntax?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
You can see that I've had to tweak our previous pig latin directive example a bit.
For starters, I moved away from a `set`ter for the input value and towards `ngOnInit`, just to ensure that everything was defined in the right timing.
@@ -1492,6 +1526,7 @@ Now, I remember when I was learning a lot of the structural directive stuff, I t
```
<iframe src="https://stackblitz.com/edit/start-to-source-34-pig-latin-non-binding?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
I was not, however, greeted by praises on my PR making this change, but rather by an error in my console:
> Can't bind to `makePiglatinCasing` since it isn't a known property of `p`
@@ -1521,6 +1556,7 @@ So if we did want to take the non-functional example above and fix it to not use
```
<iframe src="https://stackblitz.com/edit/start-to-source-35-pig-latin-normal-directive?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
### `as` to preserve values in template variable
One of my favorite tools at the microsyntax's disposal is the `as` keyword. On paper, it sounds extremely straightforward and duplicative of the `let` keyword:
@@ -1552,6 +1588,7 @@ export class AppComponent {
```
<iframe src="https://stackblitz.com/edit/start-to-source-36-as-keyword?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
While this example can be seen clearly with this usage of `ngIf` , let's try to add it into our `pigLatin` example:
```html
@@ -1559,6 +1596,7 @@ While this example can be seen clearly with this usage of `ngIf` , let's try to
```
<iframe src="https://stackblitz.com/edit/start-to-source-37-pig-latin-as-keyword-broken?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
In this example, we're expecting `'upper'` to be turned into `'UPPER'` by the `uppercase` pipe, then to be passed as the input to `makePiglatinCasing` and for the `$implicit` value of that context to be assigned to a local variable `msg`. If you load this, you'll noticed that the uppercased pig lattin displays as expected but the `upperInUpper` variable (which we expected to be `'UPPER'`) is undefined.
The reason is because we're not exporting a key of `makePiglatinCasing` in our context to supply this value.
@@ -1572,6 +1610,7 @@ this.parentViewRef.createEmbeddedView(this.templ, {
```
<iframe src="https://stackblitz.com/edit/start-to-source-38-pig-latin-as-keyword?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Now that we're exporting the output with the `as`, it should show on-screen as expected. So why is this? **Well, `as` exports the outputted value that it's bound to.** In this case, we're binding the value to `casing` (because that's what `'upper'` is being passed as an input to).
Of course, this means that you can send any value as the context. Change the code to read:
@@ -1781,6 +1820,7 @@ export class AppComponent {
```
<iframe src="https://stackblitz.com/edit/start-to-source-39-uni-for?ctl=1&embed=1&file=src/app/app.component.ts" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
- We're starting with enabling `uniFor` as the structural directive name
- Then we're defining an input to accept `of` as a key in the syntax (to match the `ngFor` structural directive syntax).

View File

@@ -0,0 +1,231 @@
---
{
title: "Introduction to Android: Contexts, Intents, and the Activity lifecycle",
description: 'A basic overview of the main components of an Android app and how they interact with each other and the Android system',
published: '2019-08-22T05:12:03.284Z',
author: 'fennifith',
tags: ['android'],
attached: [],
license: 'publicdomain-zero-1'
}
---
This is a basic summary of the different components of Android and what they can be used for. It
is written with the assumption that you already have basic knowledge about Android development,
such as Java programming and the basic construction of a simple Android app (e.g. `Activity`
classes, the `AndroidManifest.xml`, and layout files).
If you are completely new to Android development, I would recommend following through Android's
["Build your first app"](https://developer.android.com/training/basics/firstapp/) tutorial before
reading this article.
## Contexts
In Android, a `Context` is a general class that... facilitates your app's interaction with the
Android system? I'm not sure how to best explain it, but it essentially gives you access to
everything in your application from string resources and fonts to starting new Activites.
The `Application`, `Activity`, and `Service` classes all extend `Context`, and `View` classes
all require an instance of one to be displayed (you can obtain this instance by using the
View's `.getContext()` method). This allows you to access information such as the device's
screen orientation, locale, and obtain assets particular to this information. For example,
locale-specific string resources (which are commonly defined in `res/values/strings.xml`) can
be obtained by calling `context.getString(R.string.string_name)`, while Drawables (a type of
image asset) can be obtained using `context.getDrawable(R.drawable.drawable_name)`.
The `R` class that is used to obtain these resources is a collection of static identifiers
that is automatically generated by Android Studio at build/compile-time.
For more about translating strings, see the
["Localize your app"](https://developer.android.com/guide/topics/resources/localization) guide
in the Android Developer Documentation.
For more about Drawables and other image assets, see
["Drawable resources"](https://developer.android.com/guide/topics/resources/drawable-resource.html).
A general overview of app resources can be found
[here](https://developer.android.com/guide/topics/resources/providing-resources).
## Intents
Every component inside of an Android app is started by an `Intent`. Components declared in an
app's manifest can typically be invoked from _anywhere in the system_, but you can define
intent-filters to declare that they should be started by a specific type of "thing". Your app's
main activity has a filter like `android.intent.category.LAUNCHER`, which is how the home screen
knows to display and launch _that specific activity_ when the user opens your app.
Assuming that you have an active `Context`, you can start other activities inside your application
by firing an intent that references the classes directly, like:
```java
context.startActivity(new Intent(context, ActivityClass.class));
```
This call to `startActivity` sends the `Intent` to the Android system, which is then in charge of
creating and opening the activity that you have specified.
### Starting an Unknown Activity
You do not always need to specify an explicit class to start a new activity, though. How would your
app start an activity in another application? You don't know what its class name is, and if you did,
you likely wouldn't be able to reference it since it isn't a part of your app. This is where
intent-filters come in: they allow you to start an activity without explicitly stating which activity
should be launched. Take a look at the following intent:
```java
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("content://..."), "image/*");
context.startActivity(intent);
```
This intent will open any activity on the device that claims it is able to display image files from
a URI. The first (or most prominent) activity that the Android system finds with a filter that contains
the `android.intent.action.VIEW` action and accepts `image/*` data will be launched by the system.
If you want to let the user choose which activity is launched that meets the criteria, you could open
a "share menu" with `context.startActivity(Intent.createChooser(intent, "Open with..."));`. Part of the
reason that Android's share menus are notorious for being
_[so ridiculously slow](https://issuetracker.google.com/issues/68393945)_ is that in order to display
these lists, it has to query every single activity on the device asking "will you accept this Intent?"
to make a list for the user to choose from.
### Sending Data to Activities
In order for an Activity to have any dynamic functionality, you will need to have some way of sending
information to it. When you create an Intent to a new Activity, you should _never_ have an instance of
the created activity, as it is created and managed separately by the system. While under normal
circumstances this may not present any obvious issues, there are situations where this would not be possible
(for example, starting an activity in a different process or task hierarchy). However, you still need a
reliable way to tell an activity what to display while abiding by the laws of the system. There are two
main ways of doing this, both of which have their own advantages and disadvantages:
#### 1. Create your own state / data provider.
This indirectly relies on having access to an instance of the activity, though it should not fail if
it does not obtain an instance; rather than relying on the started activity being created, it acts
as more of a general solution to managing the data or state across your application.
The [Android Architecture Components](https://developer.android.com/topic/libraries/architecture/index.html)
suggest to use a [LiveData](https://developer.android.com/topic/libraries/architecture/livedata)
observable data class for this purpose, which allows you to persist a set of information across
your entire application and notify other parts of your app when it is changed. While this is a
very robust solution that will make your application much easier to maintain in the long run,
it can be a little bit complicated, especially if you are writing a simple application that
only needs to manage a small amount of information.
#### 2. Use Intent extras.
The other, much simpler method of transferring data between activities is to simply include the
data in the Intent. This data will be passed with the Intent to the Android system when it starts
the activity, which will then give it to the new Activity once it has been created. For example:
```java
Intent intent = new Intent(context, ActivityClass.class);
intent.putExtra("com.package.name.EXTRA_MESSAGE", "Hello world!"); // the data to send
context.startActivity(intent);
```
Now, when the new Activity is created (inside the `onCreate` method), you can obtain the provided
data as such:
```java
Bundle extras = getIntent().getExtras();
if (extras != null) {
String message = extras.getString("com.package.name.EXTRA_MESSAGE");
// message == "Hello World!"
}
```
Of course, this has its restrictions; since the data is passed to the system, there is a size limit
on the amount of data that you can pass through an Intent - most primitive types and small data
structures / serializable classes will be fine, but I would recommend against passing heavier classes
such as Bitmaps or Drawables.
For more information about Intents, there is a more descriptive summary of them in the
[Android Developer Documentation](https://developer.android.com/reference/android/content/Intent).
### Note: More about Contexts
When your application is opened by the system, the first components to be created will be the
`Application`, then the `Activity` - which will have a different Context from the Application
component. If you Activity contains a layout with a set of views, the context that a view has
can probably be cast to an Activity (like `(Activity) view.getContext()`) without any problems,
but it... isn't a very good idea to assume this, as there are weird situations where this might
not work.
If your app needs to have a global "thing" shared between all of its components, or if you need
to notify the parent activity of an event occurring in a view, then it is best to put that inside
of your app's `Application` class (which should be referenced from the manifest) and have your
Activities and other parts of your application look there for the information. The Application
class can be obtained from any `Context` instance by calling `context.getApplicationContext()`.
## Activity Lifecycle
Activities are big and complicated things, and many events can occur during their use as a result
of user interaction or just weird Android memory-saving things. However, it is important to know
what state of the lifecycle your Activity is in when performing a task as things can go very wrong
if you try to do something at the wrong time, or fail to stop at the right time.
You are probably familiar with the `onCreate()` method - this is the first event to happen in the
Activity lifecycle. Here, you declare and inflate your Activity's layout and set up the UI. After
this, `onStart()` and `onResume()` are called once your layout becomes visible and the user
can interact with it. From here, a few different events can occur...
Let's say that another Activity comes into the foreground (but this activity is still visible behind
it; imagine a popup or something that the user will return to your app from).
- `onPause()` called; stop doing anything significant - playing music or any continuous task not running
in another component (like a `Service`) should be ceased.
Then, if the user returns to the activity...
- `onResume()` called; resume whatever was paused previously
If the user leaves your activity completely, then you will get:
- `onPause()` called; probably stop doing stuff maybe
- `onStop()` called; okay, REALLY stop doing stuff now
Then, if the user navigates back to your activity...
- `onRestart()` called
- `onStart()` called
- `onResume()` called
When the application is completely closed by the user, then you will receive:
- `onPause()` called
- `onStop()` called
- `onDestroy()` called
A more comprehensive overview of the Activity lifecycle can be found
[here](https://developer.android.com/guide/components/activities/activity-lifecycle).
## More...
What about tasks that you want to exist beyond the Activity lifecycle? Maybe you want music to
keep playing after the user leaves the app, or you just want to perform a short action without
opening an activity when a certain event occurs. There are two other components that can receive
intents for this purpose: `Service` and `BroadcastReceiver`.
### Services
Services can run in the background without being attached to a user interface for longer periods
of time, for tasks such as playing music, downloading large files, or other potentially lengthy
operations that shouldn't be terminated when the user leaves the app.
See: [Service documentation](https://developer.android.com/reference/android/app/Service).
### Broadcast Receivers
A broadcast receiver can be seen as more of an "event" that occurs once and is over. They can run
independently from a UI, the same a Service, but only for a short period of time (I believe they are
terminated by the system after ~10 seconds - citation needed). However, they are given a `Context`,
and can fire an intent to start other components of the app if needed.
Broadcast receivers are a little special in that they don't have to be declared explicitly in the
`AndroidManifest.xml`. While Activities and Services must be declared in order to be used, broadcast
receivers can be registered dynamically when your application is running, and can be unregistered
again when they are no longer needed.
See: [BroadcastReceiver documentation](https://developer.android.com/reference/android/content/BroadcastReceiver).
## Fin
That's all for now! This was not a very thorough overview of Android development, and I feel like
I left a lot of holes and exceptions to what I mentioned here, but hopefully it is useful to someone.

View File

@@ -0,0 +1,83 @@
---
{
title: "Joining Freenode IRC: A Guide",
description: 'Basic (but detailed) instructions for setting up a Freenode IRC account through various clients',
published: '2019-08-22T05:12:03.284Z',
author: 'fennifith',
tags: ['irc'],
attached: [],
license: 'publicdomain-zero-1'
}
---
Internet Relay Chat is a difficult thing to get used to, especially for people who were born into this world of full graphical interfaces and messaging web apps that handle user interaction seamlessly. IRC is a little bit different, though it still has a lot of the functionality that conventional messengers do: group chats / channels, admin (operator) permissions, user ban lists, private messages, and _quite a bit more_. However, a lot of this functionality may seem obscured to new users, as most IRC clients don't have the fancy menus, dropdowns, or simple toggles and check box elements that are often taken for granted - they use more of a command line-like interface, having users remember the commands to execute a specific action instead, like `/motd` or `/whois fennifith`.
## Choosing a Client
The first thing that you'll want to do before logging into freenode is choose an IRC client to connect with. I've compiled a list of the ones that I have tried below.
- **Android**
- [Revolution IRC Client](https://play.google.com/store/apps/details?id=io.mrarm.irc)
- [Riot IM](https://about.riot.im/)
- [AndroIRC](https://play.google.com/store/apps/details?id=com.androirc)
- [IRCCloud](https://play.google.com/store/apps/details?id=com.irccloud.android)
- **Linux**
- **CLI**
- [WeeChat](https://weechat.org/)
- [Irssi](https://irssi.org/)
- **GUI**
- [HexChat](https://hexchat.github.io/)
- [XChat](http://xchat.org/)
- **Windows**
- [HexChat Windows](https://www.microsoft.com/en-us/p/hexchat/9nrrbgttm4j2)
- **Web**
- [Riot IM](https://riot.im/app/)
- [Freenode Webchat](https://webchat.freenode.net/)
- [Kiwi IRC](https://kiwiirc.com/)
- [The Lounge](https://demo.thelounge.chat/)
## Connecting to Freenode
Connect to the freenode servers by specifying `chat.freenode.net` as the server, and either port `6697` if your client supports SSL/TLS connections, or `6667` if it does not. Many clients have a preset option for connections to freenode, for example in `irssi` you can simply type `/CONNECT Freenode` to connect to a freenode server without needing to configure anything else.
For a more detailed explanation of connecting to freenode, [Freenode's documentation](https://freenode.net/kb/answer/chat) might be useful.
## Registering a Nickname
First, you'll want to choose a nick. This will be something that all users will see and address you by, so it should be easy to remember. If you have a twitter or github handle, it is best to make it as similar as possible to that in order to stay consistent. In the following steps, replace the information surrounded by `<>` with the relevant data.
1. Send the command `/nick <username>`, followed by a message to `NickServ` by running `/msg NickServ REGISTER <password> <email@example.com>`.
2. You should receive an email with another command to run, along the lines of `/msg NickServ VERIFY REGISTER <username> <code>`. This will confirm your identity to freenode and reserve the nickname for your use.
3. If you plan to use your account from multiple devices simultaneously, you will need to have one username for each. You can join them to your current account by:
- Setting your nick to a new username: `/nick <username2>`
- Identifying with your existing credentials: `/msg NickServ IDENTIFY <username> <password>`
- Grouping the nick with your account: `/msg NickServ GROUP`
Each time you reconnect to freenode, you will need to log in. [Freenode's registration docs](https://freenode.net/kb/answer/registration) have more information on this, but it is possible to simply run `/msg NickServ IDENTIFY <username> <password>` each time you connect.
## Joining a Channel
On most IRC servers, you can run `/list` to display a list of all of the channels on the server that you can join. However, as freenode has just shy of 50000 channels, this command will generate quite a large output that may not be to your liking. Two options here: you can either use a web index, such as [irc.netsplit.de](http://irc.netsplit.de/channels/?net=freenode), to view a list of channels in a more usable format, or you can use freenode's [alis tool](https://freenode.net/kb/answer/findingchannels) to search through the list with a query such as `/msg alis LIST programming`. Alis has quite a few other options to trim down the search results, and I reccomend taking a look at `/msg alis HELP LIST` before you start scrolling through 1000+ search results to look for a particular topic.
## General Use
By now, you've probably gotten a decent feel for how IRC chat works - most commands handle faulty input fairly gracefully and let you know what they're doing and how to use them properly. Most commands and usernames are case insensitive, and help can usually be found by simply adding `help` after the root command, ex: `/msg NickServ HELP VERIFY`. If you haven't come across them already, here is a list of various useful commands and what they do:
- `/info`: display information about the server
- `/names`: show the usernames of members in the current channel
- `/whois <username>`: looks up information about a particular user's connection
- `/msg <username> <message>`: sends a private message to a user
- `/join <channel>`: joins a particular channel
- `/me <action>`: invoke a virtual action, such as `/me takes a humongous bite of their pie` to create a notice such as "fennifith takes a humongous bite of their pie"
- `/describe <username> <description>`: similar to `/me`, using the username of someone else on the network, ex: `/describe steve012 crashes through the wall`
- `/notify <username>`: tells the server to send you a notification when another user logs on
- `/ping <username|channel>`: displays information about the distance between your computer and other users on the network
- `/quit <message>`: quits the server, sending a final comment to any chats you may be involved with
More commands, along with basic descriptions of how they work and examples of their use, can be found [here](https://www.livinginternet.com/r/r.htm).
## Policies
Last, but certainly not least, I recommend that you scroll through [freenode's policies](https://freenode.net/policies) to get an idea of the purpose of the project and what is deemed acceptable use of their servers. Most channels have their own code of conduct to go along with these policies, which you should review to make sure that you aren't unknowingly violating any rules when contributing to a discussion. The [channel guidelines](https://freenode.net/changuide) also list more definitions of what is considered to be acceptable behavior on IRC (and really any social network).
And, most importantly, have fun!

View File

@@ -0,0 +1,252 @@
---
{
title: "Continuous Integration with Travis CI for Android",
description: 'An in-depth tutorial explaining how to set up Travis CI to deploy signed builds to Google Play. Among other things',
published: '2019-08-22T05:12:03.284Z',
author: 'fennifith',
tags: ['android', 'ci'],
attached: [],
license: 'publicdomain-zero-1'
}
---
Last week, I started setting up continuous integrations for some of my projects. The basic idea of a continuous integration is that you have a server to build your project on a regular basis, verify that it works correctly, and deploy it to wherever your project is published. In this case, my project will be deployed to the releases of its GitHub repository and an alpha channel on the Google Play Store. In order to do this, I decided to use [Travis CI](https://travis-ci.com/), as it seems to be the most used and documented solution (though there are others as well). Throughout this blog, I will add small snippets of the files I am editing, but (save for the initial `.travis.yml`) never an entire file. If you get lost or would like to see a working example of this, you can find a sample project [here](/redirects/?t=github&d=TravisAndroidExample).
A small preface, make sure that you create your account on [travis-ci.com](https://travis-ci.com/), not [travis-ci.org](https://travis-ci.org/). Travis previously had their free plans on their .org site and only took paying customers on .com, but they have since begun [migrating all of their users](https://docs.travis-ci.com/user/open-source-on-travis-ci-com/) to travis-ci.com. However, for some reason they have decided _not to say anything about it_ when you create a new account, so it would be very easy to set up all of your projects on their .org site, then (X months later) realize that you have to move to .com. This isn't a huge issue, but it could be a little annoying if you have _almost 100 repositories_ like I do which you would have to change (though I have only just started using Travis, so it doesn't actually affect me). Just something to note.
## Step 1: Start your first build
There are a few basic things to do in order to build your project. Assuming that you have already [set up your account](https://docs.travis-ci.com/user/tutorial/) and authenticated it with your GitHub, you will next want to create a file named `.travis.yml` in your project's root directory. One thing to keep in mind here is that the YAML format in this file is heavily dependent on whitespace; tab characters are invalid, indents must be made only in spaces, and a sub-section or parameter **must** be indented or it will not be treated as such. To start, let's write a basic file that should properly build most up-to-date Android projects.
```yml
language: android
android:
components:
- tools
- platform-tools
- build-tools-28.0.3
- android-28
- extra-google-google_play_services
- extra-google-m2repository
- extra-android-m2repository
jdk:
- oraclejdk8
before_install:
- chmod +x gradlew
```
You will want to update the `android` and `build-tools` versions to match the respective values in your project's `build.gradle` file, and `extra-google-google_play_services` can be omitted (it will speed up build times) if you are not using it. The same goes for the `jdk`. Note the `before_install` section; statements placed there are executed before your project is built or installed (side-note: you will want to make sure `gradlew` and `gradle/wrapper` are in your version control; Travis uses them to build your project).
Now, when you commit this file to your repository (the branch should not make a difference), Travis should build your project and notify you of the result.
## Step 2. Signing APKs
So Travis _can_ successfully build your APK, but that itself is not very useful. It can do something with debug APKs, sure, but deploying them won't be very useful as they won't be under the same signature, and users won't be able to update from the existing application. So... we need a way to sign the application using an existing keystore that Travis has access to.
> LET'S UPLOAD OUR KEYSTORE TO GIT!
Not a bad idea. This will easily give Travis the ability to sign our APK. Isn't there some reason that you shouldn't share your keystore online, though, maybe something about "malicious developers and companies can use it to update your application without your knowledge"? Weeeelll why don't we use Travis's built-in encryption service? This will give you an encrypted file (like `key.jks.enc`) that you can safely add to git, and add a command to the `before_install` section in your `.travis.yml` to decrypt it.
> But... can't someone just look in your `.travis.yml`, get the command, and use it to decrypt your file?
No, they can't. This is because the values passed to the command are two [environment variables](https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-settings) which are stored only on Travis. As long as you _don't_ check the "show value in log" box when you create an environment variable, they will never be output anywhere in your build logs, and nobody will be able to see them or know what they are.
If you are worried about security (or if you aren't worried enough), I highly recommend that you read [Travis's documentation](https://docs.travis-ci.com/user/best-practices-security/#Steps-Travis-CI-takes-to-secure-your-data) on best practices regarding secure data.
### Part A. Encrypting files
You can go about this two ways: a difficult way, or a difficult way. You can either install [Travis's CLI tool](https://docs.travis-ci.com/user/encrypting-files/) for the sole purpose of logging in, encrypting your file, and setting its environment variables, or you can just do it yourself. I will provide instructions for both. Do what you like.
Note that if you want to automatically deploy your builds to Google Play, you may want to come back here and go through the exact same process later on, so you might want to skip this for now. If you don't, or want to do it twice anyway... carry on...
#### Using Travis's CLI
First, install it. Assuming you have Ruby set up, you'll want to run `gem install travis`. Since not everyone has Ruby set up, [here are their installation instructions](https://www.ruby-lang.org/en/documentation/installation/). A bit of a pain for something that you can just write yourself in my opinion, but hey, anything to avoid writing more code.
After that, you'll want to log in. Run `travis login` and it will walk you through it. Note: (related to the preface at the start) no matter what site you are using when you use the Travis CLI, you should append either `--org` or `--com` to **every command** to specify which site it should use.
Now, find your keystore. Place it in your root directory. The CLI detects git repos to determine what project you want to modify, so this is necessary. Do not add it to git. That is bad and not good. Don't do that.
Assuming you have named your keystore `key.jks`, you will want to run `travis encrypt-file key.jks --add`. This will encrypt the file, add the command to your `.travis.yml`, and upload the environment variables all at once. You can then add `key.jks.enc` to git, commit and push, and it will be available to your next build.
Side-note: if your keystore is a `.keystore` file, it shouldn't make a difference - just replace `key.jks` with `key.keystore` (or whatever it is named) whenever it appears.
#### Doing It Yourself
Pick a key and a password. They shouldn't be excessively long, but not tiny either. Do not use special characters. In this example, I will use "php" as the key and "aaaaa" as the password.
Add them to Travis CI as environment variables. You can do this by going to your project page in Travis, clicking on "More Options > Settings", then scrolling down to "Environment Variables". I will name mine "enc_keystore_key" and "enc_keystore_pass", respectively.
Now, time to encrypt the file. Run this command in the terminal:
```bash
openssl aes-256-cbc -K "php" -iv "aaaaa" -in key.jks -out key.jks.enc
```
Now, you will want to add a line to decrypt the file in `before_install` of your `.travis.yml`. You should not pass your key/password here, as this file will be pushed to git, and that would be bad. Instead, we will reference the environment variables.
```yml
before_install:
- ...
- openssl aes-256-cbc -K $enc_keystore_key -iv $enc_keystore_pass -in key.jks.enc -out key.jks -d
```
That's it! Push your changes to `.travis.yml` as well as `key.jks.enc`, and Jekyll should build your project.
### Part B. Dummy files
This isn't entirely necessary, but you can use some fake "dummy" files to add to version control alongside the "real" encrypted ones. When Travis decrypts your encrypted files, they will be overwritten, but otherwise they serve as quite a nice substitute to prevent anyone from getting their hands on the real files (and to prevent you from uploading the real ones by accident). You can find a few (`key.jks`, `service.json`, and `secrets.tar`) in the sample project [here](/redirects/?t=github&d=TravisAndroidExample).
### Part C. Signing the APK
Now we want to actually use the key to sign our APKs. This requires a few changes to our app's build.gradle. Specifically, we need to specify a `signingConfig` that ONLY exists on Travis - we don't want our local builds (or the builds of other contributors) to be affected by this. Luckily, not only can we read environment variables from our `build.gradle` file using `System.getenv`, Travis automatically creates a nice "CI" variable to tell us that the build is happening in a Continuous Integration, so why don't we use that.
Full credit, this solution was taken from [this wonderful article](https://android.jlelse.eu/using-travisci-to-securely-build-and-deploy-a-signed-version-of-your-android-app-94afdf5cf5b4) that describes almost the same thing that I have been explaining since the start of this article.
I'll create three environment variables that will be used here: the keystore password as "keystore_password", the keystore alias as "keystore_alias", and the alias's password as "keystore_alias_password". Note that special characters cannot be used in these either.
```gradle
android {
...
signingConfigs {
release
}
buildTypes {
release {
...
signingConfig signingConfigs.release
}
}
def isRunningOnTravis = System.getenv("CI") == "true"
if (isRunningOnTravis) {
signingConfigs.release.storeFile = file("../key.jks")
signingConfigs.release.storePassword = System.getenv("keystore_password")
signingConfigs.release.keyAlias = System.getenv("keystore_alias")
signingConfigs.release.keyPassword = System.getenv("keystore_alias_password")
}
}
```
Of course, Travis isn't currently building a release variant (I think it defaults to `./gradlew build`), so this `signingConfig` won't be applied. We need to change that. Add the following to your `.travis.yml`...
```yml
script:
- ./gradlew assembleRelease
```
Now it will create a proper release using these signing configs. Push everything to git and it should build a properly signed APK. Yay.
## Step 3. Deploying to github releases
This part is fairly simple, as Travis provides its own deployment functionality for this purpose. According to [their documentation](https://docs.travis-ci.com/user/deployment/releases/), for the bare minimum functionality all that you will need is to add the following to your `.travis.yml`...
```yml
deploy:
- provider: releases
api_key: "GITHUB OAUTH TOKEN"
file: app/build/outputs/apk/release/*
file_glob: true
skip_cleanup: true
on:
tags: true
```
Now, you _could_ follow this exactly and place your GitHub token directly in your `.travis.yml`, but that's just asking for trouble. Luckily, you can use MORE ENVIRONMENT VARIABLES! Enter your API key with the name ex. "GITHUB_TOKEN", and write `api_key: "$GITHUB_TOKEN"` instead.
This should now create a release with a built (and signed) APK each time there is a new tag. Fair enough; all you have to do for it to deploy is create a new tag.
### Part A. Creating tags
What if you're lazy like me, though? What if you want to create a new release on each push to the master branch? (I have two branches in most of my projects, `develop` and `master`, for this purpose - only the commits currently in production are in the `master` branch)
A simple modification to the `on` section of the previous snippet does the trick.
```yml
deploy:
...
on:
branch: master
```
Well, it almost does the trick. The thing is, since we haven't created a tag, Travis doesn't know what version number we want to use. It just creates a new release using the commit hash as a title. That isn't very good. I wonder if we could somehow get the version number from our build.gradle file and use that instead...
### Part B. Version numbers
Let's write a gradle task to print our version number! Place the following in your app's `build.gradle`.
```gradle
task printVersionName {
doLast {
println android.defaultConfig.versionName
}
}
```
Now when you run `./gradlew :app:printVersionName`, your version name should be printed in the console. Now all we have to do is use this in our deployment.
Just as there is a `before_install` section of our `.travis.yml`, there is also a `before_deploy`. As such, we can add the following:
```yml
before_deploy:
- export APP_VERSION=$(./gradlew :app:printVersionName)
```
This creates an environment variable ("APP_VERSION") containing our app's version name, which we can then reference from the actual deployment as follows...
```yml
deploy:
- provider: releases
api_key: "$GITHUB_TOKEN"
file: app/build/outputs/apk/release/*
file_glob: true
skip_cleanup: true
overwrite: true
name: "$APP_VERSION"
tag_name: "$APP_VERSION"
on:
branch: master
```
Yay! Now we have fully automated releases on each push to master. Because of the `overwrite` parameter, it will overwrite existing releases if the version number has not been changed (a new release will be created if it has), so they will always be up to date.
## Step 4. Deploying to the Play Store
Travis doesn't have a deployment for the Play Store, so we will have to use a third party tool. I found [Triple-T/gradle-play-publisher](https://github.com/Triple-T/gradle-play-publisher/), which should work, except there isn't an option to deploy an existing APK without building the project. Not only would a deployment that requires building a project _twice_ be super wasteful and take... well, twice as long, [I ran into problems signing the APK](https://jfenn.me/redirects/?t=twitter&d=status/1061620100409761792) when I tried it, so... let's not. Instead, we'll modify the `script` to run the `./gradlew publish` command when a build is triggered from the master branch.
### Part A. Setup
Setup is fairly simple; just follow the directions in the plugin's readme. However, what should we do with the JSON file? PLEASE DO NOT ADD IT TO GIT. ANYONE WITH THIS FILE HAS ACCESS TO YOUR PLAY CONSOLE. WE'RE ENCRYPTING IT.
You can either encrypt it as a separate file, or you can put them both in a tar (`tar -cvf secrets.tar key.jks service.json`), encrypt that, and run `tar -xvf secrets.tar` once it has been decrypted. I am not sure if either will affect how secure they are. I have opted for the tar method as it gives me less things to keep track of.
### Part B. Publishing
Now we can modify the `script` section of our `.travis.yml` to run the `./gradlew publish` command when a build is triggered from the master branch. This can be done using the "TRAVIS_BRANCH" environment variable which Travis handily creates for us. In other words...
```yml
script:
- if [ "$TRAVIS_BRANCH" = "master" ]; then ./gradlew publish; else ./gradlew build; fi
```
This should build a signed APK and upload it to the Play Store whenever a push is made to the `master` branch, then deploy the same APK to GitHub if it was built successfully. Important to note that using this method, the build will also fail if it has failed to upload the APK to the Play Store - so it _might_ not be an issue with your project if it results in a failure unexpectedly.
### Part C. Changelogs
Now, gradle-play-publisher requires you to specify a changelog at `app/src/main/play/release-notes/en-US/default.txt` for it to publish an APK. What if we want to use the same changelog for GitHub releases? We'll add another line to the `before_deploy` section and GitHub deployment to do so.
```yml
before_deploy:
...
- export APP_CHANGELOG=$(cat app/src/main/play/release-notes/en-US/default.txt)
deploy:
- provider: releases
...
body: "$APP_CHANGELOG"
```
## Finish
Hopefully this blog has gone over the basics of using Travis to deploy to GitHub and the Play Store. In later blogs, I hope to also cover how to implement UI and Unit tests, though I have yet to actually use them myself so I cannot yet write an article about them.
If you would like to see a working example of all of this, you can find it in a sample project [here](https://jfenn.me/redirects/?t=github&d=TravisAndroidExample).

View File

@@ -5,7 +5,8 @@
published: '2019-06-29T22:12:03.284Z',
authors: ['crutchcorn'],
tags: ['community', 'announcements'],
attached: []
attached: [],
license: 'cc-by-4'
}
---

View File

@@ -186,7 +186,7 @@ module.exports = {
resolve: "gatsby-plugin-react-svg",
options: {
rule: {
include: /\/src\/assets\/icons\/.*\.svg$/, // See below to configure properly
include: /(?:\/src\/assets\/icons\/|\\src\\assets\\icons\\).*\.svg$/,
},
},
},
@@ -209,7 +209,8 @@ module.exports = {
store: true,
attributes: { boost: 20 },
},
{ name: "content" },
{ name: "excerpt" },
{ name: "description" },
{
name: "slug",
store: true,
@@ -222,7 +223,8 @@ module.exports = {
// For any node of type MarkdownRemark, list how to resolve the fields' values
MarkdownRemark: {
title: node => node.frontmatter.title,
content: node => node.rawMarkdownBody,
excerpt: node => node.excerpt,
description: node => node.frontmatter.description,
slug: node => node.fields.slug,
authors: node => node.frontmatter.authors.name, //changed
tags: node => node.frontmatter.tags,
@@ -240,6 +242,7 @@ module.exports = {
],
mapping: {
"MarkdownRemark.frontmatter.authors": `UnicornsJson`,
"MarkdownRemark.frontmatter.license": `LicensesJson`,
"UnicornsJson.pronouns": `PronounsJson`,
"UnicornsJson.roles": `RolesJson`,
},

View File

@@ -4,7 +4,8 @@ module.exports = {
},
moduleNameMapper: {
".+\\.(css|styl|less|sass|scss)$": `identity-obj-proxy`,
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `<rootDir>/__mocks__/file-mock.js`,
".+(\/|\\\\)assets(\/|\\\\)icons(\/|\\\\).+\\.svg$": `<rootDir>/__mocks__/svg-comp-mock.js`,
".+\\.(jpg|svg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `<rootDir>/__mocks__/file-mock.js`,
},
testPathIgnorePatterns: [`node_modules`, `.cache`],
transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`],

7233
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,34 +11,34 @@
"classnames": "^2.2.6",
"css-loader": "^3.2.0",
"disqus-react": "^1.0.6",
"gatsby": "^2.13.52",
"gatsby-image": "^2.2.8",
"gatsby-plugin-feed": "^2.3.6",
"gatsby-plugin-google-analytics": "^2.1.7",
"gatsby": "^2.13.76",
"gatsby-image": "^2.2.12",
"gatsby-plugin-feed": "^2.3.7",
"gatsby-plugin-google-analytics": "^2.1.9",
"gatsby-plugin-lunr": "^1.5.2",
"gatsby-plugin-manifest": "^2.2.5",
"gatsby-plugin-offline": "^2.2.4",
"gatsby-plugin-manifest": "^2.2.8",
"gatsby-plugin-offline": "^2.2.9",
"gatsby-plugin-prefetch-google-fonts": "^1.4.3",
"gatsby-plugin-react-helmet": "^3.1.3",
"gatsby-plugin-react-helmet": "^3.1.4",
"gatsby-plugin-react-svg": "^2.1.2",
"gatsby-plugin-sass": "^2.1.4",
"gatsby-plugin-sharp": "^2.2.10",
"gatsby-plugin-sitemap": "^2.2.5",
"gatsby-plugin-sass": "^2.1.11",
"gatsby-plugin-sharp": "^2.2.16",
"gatsby-plugin-sitemap": "^2.2.8",
"gatsby-plugin-transition-link": "^1.12.4",
"gatsby-remark-autolink-headers": "^2.1.3",
"gatsby-remark-copy-linked-files": "^2.1.4",
"gatsby-remark-images": "^3.1.7",
"gatsby-remark-prismjs": "^3.3.4",
"gatsby-remark-responsive-iframe": "^2.2.4",
"gatsby-source-filesystem": "^2.1.9",
"gatsby-transformer-json": "^2.2.2",
"gatsby-transformer-remark": "^2.6.11",
"gatsby-transformer-sharp": "^2.2.5",
"gatsby-remark-autolink-headers": "^2.1.6",
"gatsby-remark-copy-linked-files": "^2.1.9",
"gatsby-remark-images": "^3.1.16",
"gatsby-remark-prismjs": "^3.3.8",
"gatsby-remark-responsive-iframe": "^2.2.7",
"gatsby-source-filesystem": "^2.1.14",
"gatsby-transformer-json": "^2.2.4",
"gatsby-transformer-remark": "^2.6.18",
"gatsby-transformer-sharp": "^2.2.10",
"node-sass": "^4.12.0",
"prismjs": "^1.17.1",
"react": "^16.8.6",
"react": "^16.9.0",
"react-breakpoints": "^3.0.3",
"react-dom": "^16.8.6",
"react-dom": "^16.9.0",
"react-helmet": "^5.2.1",
"react-pose": "^4.0.8"
},
@@ -47,19 +47,21 @@
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@storybook/addon-actions": "^5.1.10",
"@storybook/addon-knobs": "^5.1.10",
"@storybook/addon-links": "^5.1.10",
"@storybook/addons": "^5.1.10",
"@storybook/react": "^5.1.10",
"@testing-library/react": "^8.0.8",
"babel-jest": "^24.8.0",
"@storybook/addon-actions": "^5.1.11",
"@storybook/addon-knobs": "^5.1.11",
"@storybook/addon-links": "^5.1.11",
"@storybook/addons": "^5.1.11",
"@storybook/react": "^5.1.11",
"@testing-library/jest-dom": "^4.1.0",
"@testing-library/react": "^9.1.3",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"babel-preset-gatsby": "^0.2.8",
"babel-preset-gatsby": "^0.2.10",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.8.0",
"jest": "^24.9.0",
"jest-axe": "^3.2.0",
"jest-dom": "^4.0.0",
"jest-watch-typeahead": "^0.3.1",
"jest-watch-typeahead": "^0.4.0",
"prettier": "^1.18.2"
},
"homepage": "https://unicorn-utterances.com",

View File

@@ -1,7 +1,11 @@
$baseUnit: 4;
$baseUnit: 8;
$rootFontSize: #{$baseUnit * 4}px;
$fullScreenSearch: 450px;
$rootFontSize: #{$baseUnit * 2}px;
$startMediumScreenSize: 768px;
$endSmallScreenSize: $startMediumScreenSize - 1px;
$startSmallScreenSize: 440px;
$endSuperSmallScreenSize: $startSmallScreenSize - 1px;
$startSuperSmallScreenSize: 348px;
@function pxToRem($pxNum) {
$remSize: $pxNum / $rootFontSize;

View File

@@ -1,4 +1,4 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" class="strokeicon">
<path d="M13.5001 29.15C6.07223 31.0723 7.00784 25.5 3.5 25.5M13.5001 33.5V28C13.5001 28 13.4538 27.0811 13.793 26.1501C14.036 25.4833 14.5044 25.0107 14.9979 24.5008C10.2879 23.9158 5.50006 22.1279 5.50006 13.9979C5.48885 11.8881 6.04525 9.72326 7.50006 8.19521C6.81628 6.36297 6.86463 4.33773 7.63506 2.54021C7.63506 2.54021 9.25766 2.0536 13.5001 4.50002C15.1379 4.05612 16.818 4.02097 18.5001 4.00002C20.1821 4.02097 21.862 4.05612 23.4998 4.50002C27.7422 2.0536 29.3648 2.54021 29.3648 2.54021C30.1353 4.33773 30.1836 6.36297 29.4998 8.19521C30.9546 9.72326 31.5111 11.8881 31.4998 13.9979C31.4998 22.1279 26.712 23.9158 22.002 24.5008C22.4955 25.0107 22.9639 25.4833 23.2069 26.1501C23.5461 27.0811 23.4998 28 23.4998 28V33.5"
stroke="#153E67" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 952 B

View File

@@ -1,4 +1,4 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" class="strokeicon">
<path d="M18 19.5C18.8284 19.5 19.5 18.8284 19.5 18C19.5 17.1716 18.8284 16.5 18 16.5C17.1716 16.5 16.5 17.1716 16.5 18C16.5 18.8284 17.1716 19.5 18 19.5Z"
stroke="#153E67" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 9C18.8284 9 19.5 8.32843 19.5 7.5C19.5 6.67157 18.8284 6 18 6C17.1716 6 16.5 6.67157 16.5 7.5C16.5 8.32843 17.1716 9 18 9Z"

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 852 B

View File

@@ -0,0 +1,23 @@
import React, {useContext} from "react"
import DarkIcon from "../../assets/icons/dark.svg"
import LightIcon from "../../assets/icons/light.svg"
import btnStyles from "./dark-light-button.module.scss"
import {ThemeContext} from '../theme-context'
export const DarkLightButton = () => {
const {currentTheme, setTheme} = useContext(ThemeContext);
return (
<button
className={`${btnStyles.darkLightBtn} baseBtn`}
onClick={() => {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
}}
aria-pressed={currentTheme === 'light'}
aria-label={"Dark mode"}
>
{currentTheme === 'dark' ? <DarkIcon/> : <LightIcon/>}
</button>
)
}

View File

@@ -0,0 +1,12 @@
.dark-light-btn {
border-radius: 50% !important;
height: 40px;
width: 40px;
padding: 0 !important;
margin-left: auto; //make sure the button stays on the far right even on the homepage
}
.dark-light-btn svg {
height: 30px;
width: 30px;
}

View File

@@ -0,0 +1 @@
export * from "./dark-light-button"

View File

@@ -1,3 +1,8 @@
/**
* This code is currently unused and does not function as-expected
*
* It is left as a starting point for #2
*/
import React, { useMemo, useRef } from "react"
import classNames from "classnames"
import posed from "react-pose"

View File

@@ -15,6 +15,7 @@
display: flex;
justify-content: center;
height: 100%;
transition: color var(--animStyle) var(--animSpeed);
padding: 8px 16px 8px 8px;
font-size: var(--filterBarFontSize);
line-height: var(--filterBarFontSize);
@@ -150,6 +151,7 @@
}
.option.selected {
transition: color var(--animStyle) var(--animSpeed);
color: var(--darkPrimary);
}

View File

@@ -10,7 +10,7 @@
max-width: 100%;
@media screen and (max-width: #{$fullScreenSearch}) {
@media screen and (max-width: $endSmallScreenSize) {
width: 100%;
}
}
@@ -27,6 +27,7 @@ $btnPadding: 8px 16px 8px 15px;
padding: $btnPadding;
font-size: var(--filterBarFontSize);
line-height: var(--filterBarFontSize);
transition: color var(--animStyle) var(--animSpeed);
color: var(--darkPrimary);
border: none;
background: transparent;
@@ -90,6 +91,7 @@ $btnPadding: 8px 16px 8px 15px;
}
.listbox:not(.expanded):hover, .container.expanded .listbox, .container:not(.expanded) .filterButton:hover + .listbox {
transition: background var(--animStyle) var(--animSpeed);
background: var(--cardActiveBackground);
box-shadow: var(--cardActiveBoxShadow);
}
@@ -113,7 +115,7 @@ $btnPadding: 8px 16px 8px 15px;
cursor: pointer;
padding: 2px 0;
margin: 2px 0;
color: rgba(0, 0, 0, 0.58);
color: var(--lowImpactBlack);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@@ -147,15 +149,16 @@ $btnPadding: 8px 16px 8px 15px;
}
.option.selected {
transition: color var(--animStyle) var(--animSpeed);
color: var(--darkPrimary);
}
.isKeyboard .option.active {
box-shadow: 0 0 1px 1px var(--primary);
box-shadow: 0 0 1px 1px var(--primary); //might be nice to have this be the selection thingy for everything if it's fine for a11y -adueppen
}
.option.active {
color: black;
color: var(--black);
}
.maxHeightHideContainer {

View File

@@ -9,7 +9,7 @@
flex-basis: 100%;
}
@media screen and (min-width: 800px) {
@media screen and (min-width: $startMediumScreenSize) {
.iconContainer {
flex-wrap: nowrap;
justify-content: space-between;
@@ -22,7 +22,7 @@
}
}
@media screen and (max-width: 799px) {
@media screen and (max-width: ($endSmallScreenSize)) {
.midContainer {
order: 1;
margin: 16px 0 8px;
@@ -42,7 +42,7 @@
}
@media screen and (max-width: #{$fullScreenSearch}) {
@media screen and (max-width: $endSmallScreenSize) {
.searchField, .filterField {
flex-basis: 100%;
max-width: 100%;

View File

@@ -1,6 +1,6 @@
@import '../../../vars';
@media screen and (max-width: #{$fullScreenSearch}) {
@media screen and (max-width: $endSmallScreenSize) {
.container {
overflow: hidden;
width: 100%;
@@ -15,16 +15,19 @@
color: var(--darkPrimary);
transition: background var(--animSpeed) var(--animStyle),
box-shadow var(--animSpeed) var(--animStyle),
color var(--animSpeed) var(--animStyle),
border-color var(--animSpeed) var(--animStyle);
border: 1px solid transparent;
cursor: text;
}
.btn, .input, .input::placeholder {
transition: color var(--animStyle) var(--animSpeed);
color: var(--darkPrimary);
}
.btn:hover {
background: color var(--animStyle) var(--animSpeed);
background: var(--cardActiveBackground);
box-shadow: var(--cardActiveBoxShadow);
}

View File

@@ -1,8 +1,10 @@
import React from "react"
import React, { useState, useMemo, useEffect } from "react"
import { graphql, Link } from "gatsby"
import BackIcon from "../../assets/icons/back.svg"
import layoutStyles from "./layout.module.scss"
import "../../global.scss"
import { DarkLightButton } from "../dark-light-button"
import {ThemeContext, setThemeColorsToVars} from '../theme-context'
export const Layout = ({ location, children }) => {
const rootPath = `${__PATH_PREFIX__}/`
@@ -10,7 +12,28 @@ export const Layout = ({ location, children }) => {
const isBase = location.pathname === rootPath
const isBlogPost = location.pathname.startsWith(`${rootPath}posts`)
const [currentTheme, setCurrentTheme] = useState('light');
const winLocalStorage = global && global.window && global.window.localStorage;
useEffect(() => {
if (!winLocalStorage) return;
const themeName = winLocalStorage.getItem('currentTheme') || 'light'
setThemeColorsToVars(themeName);
setCurrentTheme(themeName)
}, [winLocalStorage])
const setTheme = (val) => {
setThemeColorsToVars(val);
setCurrentTheme(val);
localStorage.setItem('currentTheme', val)
}
return (
<ThemeContext.Provider value={{
currentTheme,
setTheme
}}>
<div
style={{
marginLeft: `auto`,
@@ -18,13 +41,24 @@ export const Layout = ({ location, children }) => {
}}
>
<header className={layoutStyles.header}>
{!isBase && <Link className={`${layoutStyles.backBtn} baseBtn`} to={`/`}><BackIcon/></Link>}
{
!isBase &&
<Link
className={`${layoutStyles.backBtn} baseBtn`}
to={`/`}
aria-label="Go back"
>
<BackIcon/>
</Link>
}
<DarkLightButton/>
</header>
<main className={!isBlogPost ? "listViewContent" : "postViewContent"}>{children}</main>
<footer>
{''}
</footer>
</div>
</ThemeContext.Provider>
)
}
@@ -44,6 +78,7 @@ export const authorFragmentQuery = graphql`
socials {
twitter
github
website
}
pronouns {
they
@@ -81,6 +116,13 @@ export const postFragmentQuery = graphql`
authors {
...UnicornInfo
}
license {
licenceType
footerImg
explainLink
name
displayName
}
}
fields {
slug

View File

@@ -14,5 +14,8 @@
max-width: 1200px;
padding: 0 15px;
margin: 0 auto;
display: flex;
flex-direction: row;
justify-content: space-between;
}

View File

@@ -1,8 +1,9 @@
import React, { useMemo } from "react"
import Image from "gatsby-image"
import styles from "./pic-title-header.module.scss"
import GitHub from '../../assets/icons/github.svg'
import Twitter from '../../assets/icons/twitter.svg'
import GitHubIcon from '../../assets/icons/github.svg'
import SiteIcon from '../../assets/icons/site.svg'
import TwitterIcon from '../../assets/icons/twitter.svg'
import { OutboundLink } from "gatsby-plugin-google-analytics"
const SocialBtn = ({icon, text, url, name}) => {
@@ -10,8 +11,8 @@ const SocialBtn = ({icon, text, url, name}) => {
if (name.endsWith('s')) return `${name}'`;
return `${name}'s`;
}, [name])
return <OutboundLink className='unlink baseBtn lowercase prependIcon' href={url}>
<span className={styles.svgContainer} aria-hidden={true}>{icon()}</span>
return <OutboundLink className='unlink baseBtn lowercase prependIcon' target="_blank" rel="noopener" href={url}>
<span className={styles.svgContainer} aria-hidden={true}>{icon}</span>
<span className='visually-hidden'>Link to {nameS}</span>
<span>
{text}
@@ -32,14 +33,20 @@ const SocialBtn = ({icon, text, url, name}) => {
export const PicTitleHeader = ({ image, socials, title, description, profile = false }) => {
return (
<div className={styles.container}>
<Image className={styles.headerPic} style={profile ? { borderRadius: "50%" } : {}} fixed={image}
loading={"eager"}/>
<Image
className={styles.headerPic}
style={profile ? { borderRadius: "50%" } : {}}
fixed={image}
loading={"eager"}
alt={`${title} ${profile ? 'profile picture' : 'header image'}`}
/>
<div className={styles.noMgContainer}>
<h1 className={styles.title}>{title}</h1>
<h2 className={styles.subheader}>{description}</h2>
{socials && <div className={styles.socialsContainer}>
{socials.twitter && <SocialBtn icon={Twitter} text={'Twitter'} name={title} url={`https://twitter.com/${socials.twitter}`}/>}
{socials.github && <SocialBtn icon={GitHub} text={'GitHub'} name={title} url={`https://github.com/${socials.github}`}/>}
{socials.twitter && <SocialBtn icon={<TwitterIcon/>} text={'Twitter'} name={title} url={`https://twitter.com/${socials.twitter}`}/>}
{socials.github && <SocialBtn icon={<GitHubIcon/>} text={'GitHub'} name={title} url={`https://github.com/${socials.github}`}/>}
{socials.website && <SocialBtn icon={<SiteIcon/>} text={'Website'} name={title} url={socials.website}/>}
</div>}
</div>
</div>

View File

@@ -1,3 +1,5 @@
@import '../../vars';
.container {
margin: 0 auto 50px auto;
max-width: 948px;
@@ -5,6 +7,10 @@
align-items: center;
justify-content: center;
flex-wrap: wrap;
@media screen and (max-width: $endSmallScreenSize) {
margin-bottom: 16px;
}
}
.container > * {
@@ -13,17 +19,31 @@
}
.headerPic {
max-width: 300px;
max-height: 300px;
$desktopImgSize: 300px;
max-width: $desktopImgSize;
max-height: $desktopImgSize;
margin-right: 48px;
margin-bottom: 16px;
@media screen and (max-width: $endSmallScreenSize) {
$mobileImgSize: 150px;
max-width: $mobileImgSize;
max-height: $mobileImgSize;
}
}
.title {
font-size: 64px;
line-height: 64px;
transition: color var(--animStyle) var(--animSpeed);
color: var(--highImpactBlack);
margin: 0;
@media screen and (max-width: $endSmallScreenSize) {
font-size: 40px;
line-height: 40px;
font-weight: normal;
text-align: center;
}
}
.noMgContainer {
@@ -34,29 +54,48 @@
.subheader {
margin: 22px 0 0 0;
transition: color var(--animStyle) var(--animSpeed);
color: var(--midImpactBlack);
font-size: 32px;
font-weight: 400;
white-space: pre-line;
line-height: 38px;
@media screen and (max-width: $endSmallScreenSize) {
font-size: 24px;
margin: 16px 16px 0;
}
}
.socialsContainer {
display: flex;
transition: color var(--animStyle) var(--animSpeed);
font-family: var(--oswald);
font-size: 36px;
color: var(--darkPrimary)
font-size: $baseUnit * 5px;
color: var(--darkPrimary);
flex-wrap: wrap;
}
@media screen and (max-width: $endSmallScreenSize) {
.socialsContainer {
justify-content: center;
font-size: $baseUnit * 4px;
}
}
.socialsContainer > *:not(:last-child) {
margin-right: $baseUnit * 3px;
@media screen and (max-width: $endSmallScreenSize) {
margin-right: #{$baseUnit}px;
}
}
.socialsContainer > * {
font-family: inherit;
font-size: inherit;
color: inherit;
margin-top: 10px;
}
.socialsContainer > *:not(:first-child) {
margin-left: 20px;
margin-top: #{$baseUnit}px;
}
.svgContainer {
@@ -66,7 +105,14 @@
}
.socialsContainer svg {
height: 36px;
width: 36px;
$desktopSvgSize: $baseUnit * 5px;
height: $desktopSvgSize;
width: $desktopSvgSize;
fill: none;
@media screen and (max-width: $endSmallScreenSize) {
$mobileSvgSize: $baseUnit * 4px;
height: $mobileSvgSize;
width: $mobileSvgSize;
}
}

View File

@@ -0,0 +1,22 @@
import React from "react"
import { render } from "@testing-library/react"
import { PicTitleHeader } from "./pic-title-header"
test("Renders with the expected text", async () => {
const { baseElement, findByText } = render(<PicTitleHeader
image={'https://unicorn-utterances.com/static/e32c87870d4630382a9dae8cae941af6/5f3f7/unicorn-utterances-logo-512.png'}
socials={{
website: 'http://google.com'
}}
title={"User"}
description={"Description"}
profile={true}
/>)
expect(baseElement).toBeInTheDocument();
expect(await findByText("User")).toBeInTheDocument();
expect(await findByText('Description')).toBeInTheDocument();
expect(await findByText('Link to User\'s')).toBeInTheDocument();
expect(await findByText('Website')).toBeInTheDocument();
})

View File

@@ -1,3 +1,5 @@
@import '../../vars';
.postsListContainer {
display: flex;
flex-direction: row;
@@ -8,23 +10,22 @@
text-align: center;
display: block;
max-width: 500px;
transition: color var(--animStyle) var(--animSpeed);
color: var(--midImpactBlack);
margin: 20px auto 0;
font-size: 18px;
}
}
.postListItem {
margin: 6px 0;
}
@media screen and (min-width: 800px) {
@media screen and (min-width: $startMediumScreenSize) {
.postListItem {
--marginSize: 12px;
--marginSize: 8px;
flex-basis: calc(50% - var(--marginSize));
margin: calc(var(--marginSize) / 2) var(--marginSize);
margin: var(--marginSize);
flex-grow: 0;
width: calc(50% - var(--marginSize));
max-width: calc(50% - var(--marginSize));

View File

@@ -4,9 +4,20 @@ import cardStyles from "./post-card.module.scss"
import Image from "gatsby-image"
import { stopPropCallback } from "../../utils/preventCallback"
/**
* @param {string} title - The title of the post
* @param {UnicornInfo[]} authors - Info on the authors of the post
* @param {string} published - Date the author published the post
* @param {string[]} tags - List of tags associated with the post
* @param {string} excerpt - The autogenerated excerpt from the GraphQL call
* @param {string} slug - The post URL slug (which is also it's unique ID)
* @param {string} [description] - The manually written description of the post in the post frontmatter
* @param {string} [className] - Classname to pass to the post card element
*/
export const PostCard = ({ title, authors, published, tags, excerpt, description, className, slug }) => {
const headerLink = useRef()
const authorLink = useRef()
return (
<div className={`${cardStyles.card} ${className}`} onClick={() => headerLink.current.click()}>
<div className={cardStyles.cardContents}>
@@ -15,8 +26,12 @@ export const PostCard = ({ title, authors, published, tags, excerpt, description
onClick={stopPropCallback}
className="unlink"
>
<h2 className={cardStyles.header} ref={headerLink}
>{title}</h2>
<h2
className={cardStyles.header}
ref={headerLink}
>
{title}
</h2>
</Link>
<p className={cardStyles.authorName}
onClick={(e) => {
@@ -57,7 +72,7 @@ export const PostCard = ({ title, authors, published, tags, excerpt, description
>
<Image
fixed={authors[0].profileImg.childImageSharp.smallPic}
alt={authors.name}
alt={authors[0].name}
className={cardStyles.profilePic}
imgStyle={{
borderRadius: `50%`,

View File

@@ -51,6 +51,7 @@
.authorName {
margin: 4px 0;
transition: color var(--animStyle) var(--animSpeed);
color: var(--darkPrimary);
text-decoration: underline;
// Otherwise, the div goes full width and makes clicking the author name confusing (as it all acts like a link)
@@ -72,11 +73,13 @@
.date {
position: relative;
transition: color var(--animStyle) var(--animSpeed);
color: var(--darkGrey);
margin-right: 12px;
}
.tag {
transition: color var(--animStyle) var(--animSpeed);
color: var(--darkPrimary);
}
@@ -93,6 +96,7 @@
margin-top: calc(0px - var(--halfHeight));
height: var(--height);
width: 1px;
transition: background var(--animStyle) var(--animSpeed);
background: var(--darkPrimary);
content: ' ';
}
@@ -106,7 +110,7 @@
}
.excerpt {
margin: 8px 0 0 0;
margin: var(--marginSize) 0 0 0;
position: relative;
}
@@ -116,6 +120,7 @@
height: 100%;
left: -12px;
width: 2px;
transition: background var(--animStyle) var(--animSpeed);
background: var(--darkPrimary);
content: ' ';
}

View File

@@ -0,0 +1,48 @@
import React from "react"
import { fireEvent, render } from "@testing-library/react"
import { PostCard } from "./post-card"
import { MockPost } from "../../../__mocks__/data/mock-post"
import {onLinkClick} from 'gatsby';
const {frontmatter: {tags, author, title, published, description}, excerpt, fields: {slug}} = MockPost;
describe("Post card", () => {
test("Renders with the expected text and handles clicks properly", async () => {
const { baseElement, findByText, findByTestId } = render(<PostCard
title={title}
author={author}
published={published}
tags={tags}
excerpt={excerpt}
slug={slug}
/>)
expect(baseElement).toBeInTheDocument();
expect(await findByText("by Joe")).toBeInTheDocument();
expect(await findByText('10-10-2010')).toBeInTheDocument();
expect(await findByText('item1')).toBeInTheDocument();
expect(await findByText('This would be an auto generated excerpt of the post in particular')).toBeInTheDocument();
fireEvent.click(await findByText("Post title"));
expect(onLinkClick).toHaveBeenCalledTimes(2);
fireEvent.click(await findByTestId("authorPic"));
expect(onLinkClick).toHaveBeenCalledTimes(4);
})
test("renders the description rather then excerpt", async () => {
const { findByText} = render(
<PostCard
title={title}
author={author}
published={published}
tags={tags}
excerpt={excerpt}
description={description}
slug={slug}
/>
)
expect(await findByText('This is a short description dunno why this would be this short')).toBeInTheDocument();
})
});

View File

@@ -10,16 +10,17 @@ export const PostMetadata = ({ post }) => {
return (
<div className={styles.container}>
<div onClick={() => authorLinkRef.current.click()} className='pointer'>
<Image className="circleImg" fixed={authors[0].profileImg.childImageSharp.mediumPic}/>
<Image
className={`circleImg ${styles.authorPic}`}
fixed={authors[0].profileImg.childImageSharp.mediumPic}
data-testid="post-meta-author-pic"
alt={`Profile pic for ${authors[0].name}`}
/>
</div>
<div className={styles.textDiv}>
<h2 className={styles.authorLink}>
{authors.map(author => {
return <Link to={`/unicorns/${author.id}`} ref={authorLinkRef} className={styles.authorName}>{author.name}</Link>
}).reduce((prev, curr) => {
return [prev, ", ", curr]
})} {/*it works but it's not pretty, also messes up styling a bit*/}
</h2>
<Link to={`/unicorns/${authors[0].id}`} ref={authorLinkRef} className={styles.authorLink}>
<h2 className={styles.authorName} data-testid="post-meta-author-name">{authors[0].name}</h2>
</Link>
<div className={styles.belowName}>
<p className={styles.date}>{post.frontmatter.published}</p>
<p className={styles.wordCount}>{post.wordCount.words} words</p>

View File

@@ -1,3 +1,5 @@
@import '../../../vars';
.container {
display: flex;
flex-wrap: nowrap;
@@ -5,19 +7,30 @@
max-width: 768px;
}
.textDiv {
margin-left: 24px;
}
.authorPic {
@media screen and (max-width: $endSmallScreenSize) {
$mobilePicSize: 64px;
height: $mobilePicSize;
width: $mobilePicSize;
}
}
.belowName > * {
font-weight: 400;
display: inline-block;
margin: 0 24px 0 0;
transition: color var(--animStyle) var(--animSpeed);
color: var(--lowImpactBlack);
font-size: 36px;
line-height: 36px;
position: relative;
@media screen and (max-width: $endSmallScreenSize) {
font-size: 24px;
}
}
.belowName > *:not(:last-child)::after {
@@ -26,17 +39,23 @@
right: -12px;
height: 100%;
width: 2px;
transition: background var(--animStyle) var(--animSpeed);
background: var(--darkPrimary);
}
.authorName {
font-size: 36px;
line-height: 36px;
transition: color var(--animStyle) var(--animSpeed);
color: var(--highImpactBlack);
margin: 0;
@media screen and (max-width: $endSmallScreenSize) {
font-size: 32px;
}
}
.authorLink {
transition: color var(--animStyle) var(--animSpeed);
color: var(--darkPrimary);
}

View File

@@ -1,3 +1,5 @@
@import '../../../vars';
.container {
max-width: 768px;
}
@@ -8,23 +10,39 @@
display: inline-block;
font-size: 26px;
color: var(--darkPrimary);
transition: color var(--animStyle) var(--animSpeed);
text-transform: uppercase;
margin: 0 24px 18px 0;
margin: 0 #{$baseUnit * 3}px #{$baseUnit * 2}px 0;
@media screen and (max-width: $endSmallScreenSize) {
margin: 0 #{$baseUnit * 2}px #{$baseUnit * 1}px 0;
}
}
.tags > *:not(:last-child)::after {
position: absolute;
content: ' ';
right: -12px;
right: -#{($baseUnit * 3) / 2}px;
height: 100%;
width: 2px;
transition: background var(--animStyle) var(--animSpeed);
background: var(--darkPrimary);
@media screen and (max-width: $endSmallScreenSize) {
right: -#{$baseUnit}px;
}
}
.title {
transition: color var(--animStyle) var(--animSpeed);
color: var(--highImpactBlack);
font-size: 64px;
margin: 0 0 50px 0;
@media screen and (max-width: $endSmallScreenSize) {
font-size: 50px;
margin-bottom: $baseUnit * 2px;
}
}
.subtitle {

View File

@@ -0,0 +1 @@
export * from './theme-context';

View File

@@ -0,0 +1,95 @@
import { createContext } from "react"
//css variable names might need to be changed a bit
export const darkTheme = {
//main styles
"--darkPrimary": "#E4F4FF",
"--black": "white", //🎵 we're gonna party like it's nine-teen eighty-fourrrrrrr 🎵
"--white": "black", // 2 + 2 = 5
"--darkGrey": "rgba(255, 255, 255, .64)",
"--highImpactBlack": "rgba(255, 255, 255, .87)",
"--midImpactBlack": "rgba(255, 255, 255, .64)",
"--lowImpactBlack": "rgba(255, 255, 255, .58)",
"--backgroundColor": "#072a41", //from tommy's mockup
"--cardActiveBackground": "#163954", //from tommy's mockup
"--cardActiveBoxShadow": "0px 2px 4px rgba(0, 0, 0, 0.27), inset 0px 1px 0px #435e75", //close to tommy's mockup but outset color is slightly different
"--codeBlockBackground": "#202746",
//code styles
"--codeBackgroundColor": "#161b1d",
"--textColor": "#7ea2b4",
"--stringColor": "#2d8f6f",
"--keywordColor": "#568c3b",
"--operatorColor": "#935c25",
"--punctuationColor": "#7ea2b4", //this might change
"--constantColor": "#aa05d4", //keeping this the same
"--functionColor": "#5357d2", //keeping this the same
"--selectionColor": "#7195a8",
"--commentColor": "#5a7b8c",
"--propColor": "#8a8a0f",
"--varColor": "#257fad",
"--selectorColor": "#6b6bb8",
"--urlColor": "#2d8f6f",
"--insertedUnderlineColor": "#ebf8ff",
"--highlightColor": "#d22d72",
"--lineNumbersColor": "#516d7b",
"--lineHighlightColor": "rgba(235, 248, 255, 0.2)",
"--lineHighlightFadeColor": "rgba(235, 248, 255, 0)",
};
export const lightTheme = {
//main styles
"--darkPrimary": "#153E67",
"--primary": "#127DB3",
"--black": "black",
"--white": "white",
"--darkGrey": "rgba(0, 0, 0, 0.64)",
"--highImpactBlack": "rgba(0, 0, 0, 0.87)",
"--midImpactBlack": "rgba(0, 0, 0, 0.64)",
"--lowImpactBlack": "rgba(0, 0, 0, 0.58)",
"--backgroundColor": "#E4F4FF",
"--cardActiveBackground": "#EBF6FC",
"--cardActiveBoxShadow": "0px 2px 4px rgba(11, 37, 104, 0.27), inset 0px 1px 0px #FFFFFF",
"--codeBlockBackground": "white",
//code styles
"--codeBackgroundColor": "#fff",
"--textColor": "#5e6687",
"--stringColor": "#007396",
"--keywordColor": "#846c00",
"--operatorColor": "#b74c00",
"--punctuationColor": "#006fce",
"--constantColor": "#aa05d4",
"--functionColor": "#5357d2",
"--selectionColor": "#dfe2f1",
"--commentColor": "#898ea4",
"--propColor": "#c08b30",
"--varColor": "#3d8fd1",
"--selectorColor": "#6679cc",
"--urlColor": "#22a9c9",
"--insertedUnderlineColor": "#202746",
"--highlightColor": "#c94922",
"--lineNumbersColor": "#979db4",
"--lineHighlightColor": "rgba(107, 115, 148, 0.2)",
"--lineHighlightFadeColor": "rgba(107, 115, 148, 0)",
}
export function setThemeColorsToVars(themeName) {
const themeObj = themeName === "dark" ? darkTheme : lightTheme;
const style = document.documentElement.style;
const themeColor = document.querySelector("meta[name='theme-color']");
// For test environments, etc
if (!themeColor) return;
themeColor.setAttribute("content", themeObj["--backgroundColor"]);
Object.entries(themeObj).forEach(([themeKey, themeVal]) => {
style.setProperty(themeKey, themeVal);
})
}
// We only have dark and light right now
export const defaultThemeContextVal = {
currentTheme: "light",
setTheme: (val) => {}
}
export const ThemeContext = createContext(defaultThemeContextVal);

BIN
src/data/adueppen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/data/fennifith.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

26
src/data/licenses.json Normal file
View File

@@ -0,0 +1,26 @@
[
{
"id": "cc-by-nc-sa-4",
"footerImg": "https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png",
"licenceType": "Creative Commons License",
"explainLink": "http://creativecommons.org/licenses/by-nc-sa/4.0/",
"name": "Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)",
"displayName": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License"
},
{
"id": "cc-by-4",
"footerImg": "https://i.creativecommons.org/l/by/4.0/88x31.png",
"licenceType": "Creative Commons License",
"explainLink": "http://creativecommons.org/licenses/by/4.0/",
"name": "Attribution 4.0 International (CC BY 4.0)",
"displayName": "Creative Commons Attribution 4.0 International License"
},
{
"id": "publicdomain-zero-1",
"footerImg": "https://licensebuttons.net/p/zero/1.0/88x31.png",
"licenceType": "Public Domain",
"explainLink": "https://creativecommons.org/publicdomain/zero/1.0/",
"name": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
"displayName": "Public Domain"
}
]

BIN
src/data/tommyemo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -4,7 +4,7 @@
"name": "Corbin Crutchley",
"firstName": "Corbin",
"lastName": "Crutchley",
"description": "I'm the ONE and only Corbin, aka:\nThe Uni-Corn \uD83E\uDD84\uD83C\uDF3D\nI do development of all kinds, usually focusing on web development.\nI collect retro video games \uD83C\uDFAE, like reading \uD83D\uDCD6, love teaching \uD83D\uDC68\u200D\uD83C\uDFEB",
"description": "I'm the ONE and only Corbin, aka:\nThe Uni-Corn 🦄🌽\nI do development of all kinds, usually focusing on web development.\nI collect retro video games 🎮, like reading 📖, love teaching 👨‍🏫",
"socials": {
"twitter": "crutchcorn",
"github": "crutchcorn"
@@ -12,9 +12,84 @@
"pronouns": "they/themselves",
"profileImg": "./crutchcorn.png",
"color": "#ba68c8",
"roles": ["devops", "developer"]
},
{
"id": "fennifith",
"name": "James Fenn",
"firstName": "James",
"lastName": "Fenn",
"description": "Enjoys writing software on loud keyboards. Starts too many projects. Consumes food.",
"socials": {
"twitter": "fennifith",
"github": "fennifith"
},
"pronouns": "he",
"profileImg": "./fennifith.jpg",
"color": "#0091EA",
"roles": ["developer"]
},
{
"id": "evelynhathaway",
"name": "Evelyn Hathaway",
"firstName": "Evelyn",
"lastName": "Hathaway",
"description": "👩‍💻🌈 I'm a student and software developer with a strong passion for frontend and backend JavaScript and web accessibility.",
"socials": {
"twitter": "evehathdev",
"github": "evelynhathaway"
},
"pronouns": "she",
"profileImg": "../../content/assets/branding/emotes/proud.png",
"color": "#ef5f17",
"roles": ["developer", "devops"]
},
{
"id": "adueppen",
"name": "Alex Dueppen",
"firstName": "Alex",
"lastName": "Dueppen",
"description": "I do stuff sometimes.",
"socials": {
"twitter": "AlexDueppen",
"github": "adueppen",
"website": "https://ajd.sh/"
},
"pronouns": "he",
"profileImg": "./adueppen.png",
"color": "#69ffff",
"roles": ["developer"]
},
{
"id": "zavukodlak",
"name": "vukashin",
"firstName": "vukashin",
"lastName": "",
"description": "I'm a high school student doing creative work on the internet.",
"socials": {
"twitter": "vukash_in",
"website": "https://vukash.in/"
},
"pronouns": "he",
"profileImg": "./vukashin.png",
"color": "#3485FF",
"roles": ["designer"]
},
{
"id": "tommyemo",
"name": "Tom Wellington",
"firstName": "Tom",
"lastName": "Wellington",
"description": "I design icons and user interfaces, among other things. he/him ✌️",
"socials": {
"twitter": "tommy_emo_",
"website": "https://www.tommyemo.net/"
},
"pronouns": "he",
"profileImg": "./tommyemo.jpg",
"color": "#8539EB",
"roles": ["designer"]
},
{
"id": "test1",
"name": "Test 1",
@@ -30,7 +105,6 @@
"color": "#ff0000",
"roles": ["developer"]
},
{
"id": "test2",
"name": "Test 2",

BIN
src/data/vukashin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -12,34 +12,45 @@
--midImpactBlack: rgba(0, 0, 0, 0.64);
--lowImpactBlack: rgba(0, 0, 0, 0.58);
--backgroundColor: #E4F4FF;
--cardActiveBackground: rgb(235, 246, 252);
--cardActiveBackground: #EBF6FC;
--cardActiveBoxShadow: 0px 2px 4px rgba(11, 37, 104, 0.27), inset 0px 1px 0px #FFFFFF;
--animSpeed: 200ms;
--animStyle: ease-in-out;
--cardOutlineStyle: 1px solid var(--primary);
--archivo: 'Archivo', sans-serif;
--oswald: 'Oswald', 'Archivo', sans-serif;
--cardRadius: #{2 * $baseUnit}px;
--filterBarIconSize: #{6 * $baseUnit}px;
--filterBarFontSize: #{5 * $baseUnit}px;
--listViewPadding: #{3 * $baseUnit}px;
--cardRadius: #{$baseUnit}px;
--filterBarIconSize: #{3 * $baseUnit}px;
--filterBarFontSize: #{2.5 * $baseUnit}px;
--listViewPadding: #{1.5 * $baseUnit}px;
font-size: $rootFontSize;
line-height: 1.2;
}
@media screen and (min-width: 600px) {
@media screen and (min-width: $startMediumScreenSize) {
:root {
--listViewPadding: #{$baseUnit * 5}px;
--listViewPadding: #{$baseUnit * 2.5}px;
}
}
:focus {
outline-color: var(--darkPrimary);
}
//without this all <button>s have bad outline colors in firefox
:focus::-moz-focus-inner {
padding: 0; //prevent weirdness just in case
border-color: var(--darkPrimary);
}
.listViewContent {
margin: 0 auto;
max-width: #{67318 * $baseUnit}px;
max-width: #{160 * $baseUnit}px; // 1280px
padding: 0 var(--listViewPadding)
}
.postViewContent {
padding: #{$baseUnit * 5}px;
padding: #{$baseUnit * 2.5}px;
}
.postViewContent > * {
@@ -49,12 +60,22 @@
body {
background-color: var(--backgroundColor);
font-family: var(--archivo);
color: var(--black);
transition: color var(--animStyle) var(--animSpeed), background-color var(--animStyle) var(--animSpeed);
}
h1, h2, h3 {
font-family: var(--oswald);
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
/* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility */
.visually-hidden {
position: absolute !important;
@@ -94,15 +115,16 @@ h1, h2, h3 {
background: none;
border: none;
transition: background var(--animSpeed) var(--animStyle),
box-shadow var(--animSpeed) var(--animStyle),
border-color var(--animSpeed) var(--animStyle);
box-shadow var(--animSpeed) var(--animStyle),
border-color var(--animSpeed) var(--animStyle),
color var(--animStyle) var(--animSpeed);
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: var(--cardRadius);
font-size: #{$baseUnit * 4}px;
padding: #{$baseUnit}px #{$baseUnit * 2}px;
font-size: #{$baseUnit * 2}px;
padding: #{$baseUnit / 2}px #{$baseUnit}px;
color: var(--darkPrimary)
}
@@ -113,13 +135,13 @@ h1, h2, h3 {
}
.baseBtn svg, .btnLike svg {
$size: #{$baseUnit * 8}px;
$size: #{$baseUnit * 4}px;
height: $size;
width: $size;
flex-shrink: 0;
}
$pendIconMarg: #{$baseUnit * 2}px;
$pendIconMarg: #{$baseUnit}px;
.baseBtn.prependIcon svg, .btnLike.prependIcon svg {
margin-right: $pendIconMarg;
@@ -130,28 +152,24 @@ $pendIconMarg: #{$baseUnit * 2}px;
}
.post-body {
margin-bottom: #{$baseUnit * 10}px;
max-width: #{$baseUnit * 175}px;
margin-bottom: #{$baseUnit * 4}px;
max-width: #{$baseUnit * 88}px;
img {
margin: 0 auto;
display: block;
max-width: 100%;
}
p {
line-height: 1.2em;
}
}
.post-lower-area {
max-width: #{$baseUnit * 230}px;
max-width: #{$baseUnit * 115}px;
}
// Please use this sparingly. There's massive A11y concerns
.unlink {
text-decoration: none;
color: initial;
color: inherit;
}
pre {
@@ -178,3 +196,17 @@ pre {
a {
color: var(--darkPrimary);
}
svg.strokeicon {
&, * {
transition: stroke var(--animStyle) var(--animSpeed);
stroke: var(--darkPrimary);
}
}
svg:not(.strokeicon) {
&, * {
transition: fill var(--animStyle) var(--animSpeed);
fill: var(--darkPrimary);
}
}

View File

@@ -0,0 +1,76 @@
/**
* This test is in the `__tests__` directory
*/
import React from "react"
import { fireEvent, render } from "@testing-library/react"
import ReactDOMServer from 'react-dom/server';
import { axe } from 'jest-axe';
import {onLinkClick, useStaticQuery} from 'gatsby';
import { siteMetadata } from "../../../__mocks__/data/mock-site-metadata"
import { MockPost } from "../../../__mocks__/data/mock-post"
import { MockUnicorn } from "../../../__mocks__/data/mock-unicorn"
import BlogIndex from "../index"
beforeAll(() => {
useStaticQuery.mockImplementation(() => ({
site: {
siteMetadata
}
}))
})
afterAll(() => {
useStaticQuery.mockImplementation(jest.fn())
})
const getElement = () => (
<BlogIndex
data={{
site: {
siteMetadata
},
unicornsJson: MockUnicorn,
allMarkdownRemark: {
totalCount: 1,
edges: [{
node: MockPost
}]
},
file: {
childImageSharp: {
smallPic: {
fixed: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
},
}
}
}}
location={{
pathname: '/post/this-post-name-here'
}}
/>
);
test("Blog index page renders", async () => {
const { baseElement, findByText, findByTestId } = render(getElement());
expect(baseElement).toBeInTheDocument();
fireEvent.click(await findByText('Read More'));
expect(onLinkClick).toHaveBeenCalledTimes(1)
// Post cards
expect(await findByText("by Joe")).toBeInTheDocument();
expect(await findByText('10-10-2010')).toBeInTheDocument();
expect(await findByText('This is a short description dunno why this would be this short')).toBeInTheDocument();
fireEvent.click(await findByText("Post title"));
expect(onLinkClick).toHaveBeenCalledTimes(3);
fireEvent.click(await findByTestId("authorPic"));
expect(onLinkClick).toHaveBeenCalledTimes(5);
});
test("Blog index page should not have axe errors", async () => {
const html = ReactDOMServer.renderToString(getElement());
const results = await axe(html);
expect(results).toHaveNoViolations();
});

View File

@@ -4,7 +4,7 @@ import { Layout } from "../components/layout/layout"
import { SEO } from "../components/seo"
import Image from "gatsby-image"
import style from "./about.module.scss"
import {navigate} from '@reach/router';
import { navigate } from "@reach/router"
const AboutUs = (props) => {
const { data: { markdownRemark } } = props
@@ -69,14 +69,21 @@ const AboutUs = (props) => {
<div className={style.nameRoleDiv}>
<Link to={`/unicorns/${unicornInfo.id}`}>{unicornInfo.name}</Link>
<ul aria-label="Roles assigned to this user" className={style.rolesList}>
{unicornInfo.roles.map((role, i) => (
{unicornInfo.roles.map((role, i, arr) => (
<li key={role.id}>
{i !== 0 && ", "}{role.prettyname}
{role.prettyname}
{
(arr[i + 1] || (
unicornInfo.fields.isAuthor &&
i === arr.length - 1
)) &&
<span aria-hidden={true}>,&nbsp;</span>
}
</li>
))}
{
unicornInfo.fields.isAuthor &&
<li>{unicornInfo.roles.length >= 1 && ", "}Author</li>
<li>Author</li>
}
</ul>
</div>

View File

@@ -1,3 +1,5 @@
@import "../vars";
.container {
font-size: 20px;
margin-bottom: 40px;
@@ -6,9 +8,22 @@
font-size: 64px;
}
@media screen and (max-width: $startSmallScreenSize) {
h1 {
font-size: 3rem;
}
}
@media screen and (max-width: $startSuperSmallScreenSize) {
h1 {
font-size: 2rem;
}
}
blockquote {
font-size: 32px;
margin-left: 0;
transition: color var(--animStyle) var(--animSpeed);
color: var(--midImpactBlack);
&::before {
display: none;
@@ -41,6 +56,7 @@
margin: 8px 0 0;
li {
display: inline-block;
transition: color var(--animStyle) var(--animSpeed);
color: var(--midImpactBlack);
}
}

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from "react"
import { graphql } from "gatsby"
import { graphql, Link } from "gatsby"
import {Layout} from "../components/layout/layout"
import { SEO } from "../components/seo"
import { PostList } from "../components/post-card-list"
@@ -21,7 +21,7 @@ const BlogIndex = (props) => {
const Description = <>
{data.site.siteMetadata.description}
<br/>
<a href="./about">Read More</a>
<Link to={"/about"}>Read More</Link>
</>
return (

View File

@@ -8,13 +8,25 @@ Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/b
Theme modified for usage in Unicorn Utterances by Corbin Crutchley (https://github.com/crutchcorn)
*/
:root {
--codeBackgroundColor: #fff;
--textColor: #5e6687;
--stringColor: #007396;
--keywordColor: #846c00;
--operatorColor: #b74c00;
--punctuationColor: #006fce;
--constantColor: #aa05d4;
--functionColor: #5357d2;
//008000
--selectionColor: #dfe2f1;
--commentColor: #898ea4;
--propColor: #c08b30;
--varColor: #3d8fd1;
--selectorColor: #6679cc;
--urlColor: #22a9c9;
--insertedUnderlineColor: #202746;
--highlightColor: #c94922;
--lineNumbersColor: #979db4;
--lineHighlightColor: rgba(107, 115, 148, 0.2);
--lineHighlightFadeColor: rgba(107, 115, 148, 0);
}
code[class*="language-"],
@@ -37,7 +49,8 @@ pre[class*="language-"] {
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
color: #5e6687;
color: var(--textColor);
background: var(--codeBackgroundColor);
}
pre[class*="language-"]::-moz-selection,
@@ -45,7 +58,7 @@ pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #dfe2f1;
background: var(--selectionColor);
}
pre[class*="language-"]::selection,
@@ -53,7 +66,7 @@ pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #dfe2f1;
background: var(--selectionColor);
}
/* Code blocks */
@@ -67,7 +80,7 @@ pre[class*="language-"] {
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
background: white;
background: var(--white);
white-space: normal;
}
@@ -75,7 +88,7 @@ pre[class*="language-"] {
.token.prolog,
.token.doctype,
.token.cdata {
color: #898ea4;
color: var(--commentColor);
}
.token.punctuation {
@@ -97,11 +110,11 @@ pre[class*="language-"] {
}
.token.property {
color: #c08b30;
color: var(--propColor);
}
.token.tag {
color: #3d8fd1;
color: var(--varColor);
}
.token.string {
@@ -109,7 +122,7 @@ pre[class*="language-"] {
}
.token.selector {
color: #6679cc;
color: var(--selectorColor);
}
.token.attr-name {
@@ -120,7 +133,7 @@ pre[class*="language-"] {
.token.url,
.language-css .token.string,
.style .token.string {
color: #22a2c9;
color: var(--urlColor);
}
.token.attr-value,
@@ -143,7 +156,7 @@ pre[class*="language-"] {
.token.placeholder,
.token.variable {
color: #3d8fd1;
color: var(--varColor);
}
.token.deleted {
@@ -151,7 +164,7 @@ pre[class*="language-"] {
}
.token.inserted {
border-bottom: 1px dotted #202746;
border-bottom: 1px dotted var(--insertedUnderlineColor);
text-decoration: none;
}
@@ -165,7 +178,7 @@ pre[class*="language-"] {
}
.token.important {
color: #c94922;
color: var(--highlightColor);
}
.token.entity {
@@ -173,7 +186,7 @@ pre[class*="language-"] {
}
pre > code.highlight {
outline: 0.4em solid #c94922;
outline: 0.4em solid var(--highlightColor);
outline-offset: 0.4em;
}
@@ -181,34 +194,34 @@ pre > code.highlight {
* http://prismjs.com/plugins/line-numbers/
*/
.line-numbers .line-numbers-rows {
border-right-color: #dfe2f1;
border-right-color: var(--selectionColor);
}
.line-numbers-rows > span:before {
color: #979db4;
color: var(--lineNumbersColor);
}
/* overrides color-values for the Line Highlight plugin
* http://prismjs.com/plugins/line-highlight/
*/
.line-highlight {
background: rgba(107, 115, 148, 0.2);
background: var(--lineHighlightColor);
background: -webkit-linear-gradient(
left,
rgba(107, 115, 148, 0.2) 70%,
rgba(107, 115, 148, 0)
var(--lineHighlightColor) 70%,
var(--lineHighlightFadeColor)
);
background: linear-gradient(
to right,
rgba(107, 115, 148, 0.2) 70%,
rgba(107, 115, 148, 0)
var(--lineHighlightColor) 70%,
var(--lineHighlightFadeColor)
);
}
blockquote {
position: relative;
margin-left: 1em;
color: rgba(0, 0, 0, 0.6);
color: var(--midImpactBlack);
&::before {
content: " ";
@@ -221,9 +234,9 @@ blockquote {
left: -1em;
}
a {
/*a {
color: rgba(0, 10, 125, 0.6);
}
}*/
}

Some files were not shown because too many files have changed in this diff Show More